From 0d01f316bfa23f8767438e412419bb8ef22cabf9 Mon Sep 17 00:00:00 2001 From: Simon Chae Date: Mon, 29 Jun 2020 12:45:54 -0400 Subject: [PATCH] Closes #2988: Remove all pocket references --- app/build.gradle | 1 - .../helpers/CustomPocketFeedStateProvider.kt | 33 ---- .../tv/firefox/ui/YouTubeNavigationTest.kt | 174 ---------------- .../firefox/webrender/WebRenderComponents.kt | 6 +- .../org/mozilla/tv/firefox/IntentValidator.kt | 8 - .../firefox/architecture/ViewModelFactory.kt | 6 - .../tv/firefox/channels/ChannelConfig.kt | 13 -- .../tv/firefox/channels/ChannelRepo.kt | 1 - .../tv/firefox/channels/ChannelTile.kt | 3 +- .../firefox/channels/DefaultChannelAdapter.kt | 14 +- .../tv/firefox/pocket/PocketEndpointRaw.kt | 50 ----- .../pocket/PocketVideoFetchScheduler.kt | 146 -------------- .../firefox/pocket/PocketVideoFetchWorker.kt | 43 ---- .../tv/firefox/pocket/PocketVideoParser.kt | 63 ------ .../tv/firefox/pocket/PocketVideoRepo.kt | 72 ------- .../tv/firefox/pocket/PocketVideoStore.kt | 109 ---------- .../tv/firefox/pocket/PocketViewModel.kt | 104 ---------- .../tv/firefox/telemetry/TelemetryFactory.kt | 2 - .../firefox/telemetry/TelemetryIntegration.kt | 53 ----- .../tv/firefox/utils/BuildConfigDerivables.kt | 33 ---- .../tv/firefox/utils/ServiceLocator.kt | 20 -- .../java/org/mozilla/tv/firefox/utils/URLs.kt | 1 - .../CustomContentRequestInterceptor.kt | 2 +- app/src/main/res/layout/home_tile.xml | 28 --- .../FirefoxViewModelProvidersTest.kt | 4 +- .../tv/firefox/helpers/PocketTestData.kt | 24 --- .../OverlayHintViewModelTest.kt | 1 - .../pocket/PocketVideoFetchSchedulerTest.kt | 155 --------------- .../pocket/PocketVideoFetchWorkerTest.kt | 92 --------- .../pocket/PocketVideoJSONValidatorTest.kt | 71 ------- .../firefox/pocket/PocketVideoParserTest.kt | 99 ---------- .../tv/firefox/pocket/PocketVideoRepoTest.kt | 113 ----------- .../tv/firefox/pocket/PocketVideoStoreTest.kt | 187 ------------------ .../tv/firefox/pocket/PocketViewModelTest.kt | 92 --------- .../webrender/WebRenderHintViewModelTest.kt | 1 - 35 files changed, 8 insertions(+), 1816 deletions(-) delete mode 100644 app/src/androidTest/java/org/mozilla/tv/firefox/helpers/CustomPocketFeedStateProvider.kt delete mode 100644 app/src/androidTest/java/org/mozilla/tv/firefox/ui/YouTubeNavigationTest.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketEndpointRaw.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchScheduler.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorker.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoParser.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoRepo.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoStore.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/pocket/PocketViewModel.kt delete mode 100644 app/src/main/java/org/mozilla/tv/firefox/utils/BuildConfigDerivables.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/helpers/PocketTestData.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchSchedulerTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorkerTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoJSONValidatorTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoParserTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoRepoTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoStoreTest.kt delete mode 100644 app/src/test/java/org/mozilla/tv/firefox/pocket/PocketViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index 2eaf53458e..2347e5bec6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -299,7 +299,6 @@ def addSecretToBuildConfig(variant, fieldName, fileBaseName) { android.applicationVariants.all { variant -> addSecretToBuildConfig(variant, 'SENTRY_DSN', ".sentry_dsn") - addSecretToBuildConfig(variant, 'POCKET_KEY', ".pocket_key") } // ------------------------------------------------------------------------------------------------- diff --git a/app/src/androidTest/java/org/mozilla/tv/firefox/helpers/CustomPocketFeedStateProvider.kt b/app/src/androidTest/java/org/mozilla/tv/firefox/helpers/CustomPocketFeedStateProvider.kt deleted file mode 100644 index 10ce352747..0000000000 --- a/app/src/androidTest/java/org/mozilla/tv/firefox/helpers/CustomPocketFeedStateProvider.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -@file:Suppress("DEPRECATION") // PocketVideoParser - -package org.mozilla.tv.firefox.helpers - -import android.app.Application -import io.reactivex.subjects.BehaviorSubject -import org.mozilla.tv.firefox.pocket.PocketVideoJSONValidator -import org.mozilla.tv.firefox.pocket.PocketVideoParser -import org.mozilla.tv.firefox.pocket.PocketVideoRepo -import org.mozilla.tv.firefox.pocket.PocketVideoStore - -/** - * Provides a fake [PocketVideoRepo] implementation for testing purposes. - * - * Any values pushed to [fakedPocketRepoState] will be immediately emitted. - */ -class CustomPocketFeedStateProvider(appContext: Application) { - - val fakedPocketRepoState = BehaviorSubject.create() - val fakedPocketRepo = PocketVideoRepo( - PocketVideoStore( - appContext, - appContext.assets, - PocketVideoJSONValidator(PocketVideoParser) - ), - isPocketEnabledByLocale = { true }, - _feedState = fakedPocketRepoState - ) -} diff --git a/app/src/androidTest/java/org/mozilla/tv/firefox/ui/YouTubeNavigationTest.kt b/app/src/androidTest/java/org/mozilla/tv/firefox/ui/YouTubeNavigationTest.kt deleted file mode 100644 index 6c87befbcc..0000000000 --- a/app/src/androidTest/java/org/mozilla/tv/firefox/ui/YouTubeNavigationTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.ui - -import android.app.Application -import okhttp3.mockwebserver.MockWebServer -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.mozilla.tv.firefox.TestDependencyFactory -import org.mozilla.tv.firefox.ext.toUri -import org.mozilla.tv.firefox.helpers.AndroidAssetDispatcher -import org.mozilla.tv.firefox.helpers.CustomPocketFeedStateProvider -import org.mozilla.tv.firefox.helpers.MainActivityTestRule -import org.mozilla.tv.firefox.helpers.TestAssetHelper -import org.mozilla.tv.firefox.pocket.PocketVideoRepo -import org.mozilla.tv.firefox.pocket.PocketViewModel -import org.mozilla.tv.firefox.ui.robots.browser -import org.mozilla.tv.firefox.ui.robots.navigationOverlay -import org.mozilla.tv.firefox.utils.ServiceLocator - -/** - * A test for YouTube navigation including: - * - Going back with hardware back acts as expected from YouTube tile and Pocket videos - * - D-pad navigation works as expected - * - Checking that clicking on the YouTube tile, opening the overlay, waiting for loading to end, then closing the overlay - * does not disrupt navigation - */ -class YouTubeNavigationTest { - companion object : TestDependencyFactory { - - lateinit var customPocketFeedStateProvider: CustomPocketFeedStateProvider - - override fun createServiceLocator(app: Application) = object : ServiceLocator(app) { - init { - customPocketFeedStateProvider = CustomPocketFeedStateProvider(app) - } - - override val pocketRepo = customPocketFeedStateProvider.fakedPocketRepo - } - } - - @get:Rule val activityTestRule = MainActivityTestRule() - private lateinit var server: MockWebServer - private lateinit var pages: List - - /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. - - @Before - fun setup() { - server = MockWebServer().apply { - setDispatcher(AndroidAssetDispatcher()) - start() - } - - pages = TestAssetHelper.getGenericAssets(server) - - val mockedState = PocketVideoRepo.FeedState.LoadComplete(listOf( - PocketViewModel.FeedItem.Video( - id = 27587, - title = "How a Master Pastry Chef Uses Architecture to Make Sky High Pastries", - url = "https://www.youtube.com/tv#/watch/video/idle?v=953Qt4FnAcU", - thumbnailURL = "https://img-getpocket.cdn.mozilla.net/direct?url=http%3A%2F%2Fimg.youtube.com%2Fvi%2F953Qt4FnAcU%2Fmaxresdefault.jpg&resize=w450", - popularitySortId = 20, - authors = "Eater" - ) - )) - customPocketFeedStateProvider.fakedPocketRepoState.onNext(mockedState) - } - - @Ignore("Disabled due to the Youtube warning dialog") - @Test - fun youtubeNavigationTest() { - val youtubeUrl = "youtube.com/tv" - /** - * YouTube TV, Test page 1, back to YouTube, Test page 2, back to YouTube, back out of YouTube - * Expected: Overlay - */ - navigationOverlay { - }.enterUrlAndEnterToBrowser(youtubeUrl.toUri()!!) { - }.openOverlay { - waitUntilYouTubeHomeLoads() - }.enterUrlAndEnterToBrowser(pages[0].url) { - remoteBack() - }.openOverlay { - waitUntilYouTubeHomeLoads() - }.enterUrlAndEnterToBrowser(pages[1].url) { - remoteBack() - }.openOverlay { - waitUntilYouTubeHomeLoads() - assertURLBarTextContains(youtubeUrl) - }.closeToBrowser { - backOutOfYouTube() - }.openOverlay { - assertURLBarDisplaysHint() - - /** - * Test page 3, YouTube TV, Test page 2, back to YouTube, back out of Youtube - * Expected: Test page 3 - */ - }.enterUrlAndEnterToBrowser(pages[2].url) { - }.openOverlay { - }.enterUrlAndEnterToBrowser(youtubeUrl.toUri()!!) { - }.openOverlay { - waitUntilYouTubeHomeLoads() - }.enterUrlAndEnterToBrowser(pages[1].url) { - remoteBack() - }.openOverlay { - waitUntilYouTubeHomeLoads() - assertURLBarTextContains(youtubeUrl) - }.closeToBrowser { - backOutOfYouTube() - }.openOverlay { - assertURLBarTextContains(pages[2].url.toString()) - } - - /** - * First, go back to the beginning of history. - * YouTube TV, back out of YouTube - * Expected: Overlay - */ - navigationOverlay { - waitForURLBarToDisplayHint() - assertURLBarDisplaysHint() - }.enterUrlAndEnterToBrowser(youtubeUrl.toUri()!!) { - }.openOverlay { - waitUntilYouTubeHomeLoads() - }.closeToBrowser { - backOutOfYouTube() - }.openOverlay { - assertURLBarDisplaysHint() - - /** - * Open YouTube, open overlay before loading is complete, wait for loading to complete, - * make sure YouTube navigation still works (regression test for #1830). - */ - disableSessionIdling(activityTestRule) - } - .enterUrlAndEnterToBrowser(youtubeUrl.toUri()!!) { - }.openOverlay { - enableSessionIdling(activityTestRule) - }.closeToBrowser { - /** - * Test dpad navigation. - * The only unique attribute between video elements is the 'aria-label'. - * Compare this label before and after dpad navigation to ensure focus changes. - */ - val ariaLabelAttribute = "getAttribute('aria-label')" - val firstElementLabel = getActiveElementAttribute(ariaLabelAttribute) - dpadRight() - assert(firstElementLabel != getActiveElementAttribute(ariaLabelAttribute)) - dpadLeft() - assert(firstElementLabel == getActiveElementAttribute(ariaLabelAttribute)) - } - } - - /** - * Clicks remote back until the active element is in the YouTube sidebar. - * Then, clicks remote back one more time to go to the previous site. - */ - private fun backOutOfYouTube() { - browser { - var activeElementParentId = getActiveElementAttribute("parentElement.parentElement.id") - while (activeElementParentId != "guide-list") { - remoteBack() - activeElementParentId = getActiveElementAttribute("parentElement.parentElement.id") - } - remoteBack() - } - } -} diff --git a/app/src/gecko/java/org/mozilla/tv/firefox/webrender/WebRenderComponents.kt b/app/src/gecko/java/org/mozilla/tv/firefox/webrender/WebRenderComponents.kt index fae2b6ecf7..fedd8c64e2 100644 --- a/app/src/gecko/java/org/mozilla/tv/firefox/webrender/WebRenderComponents.kt +++ b/app/src/gecko/java/org/mozilla/tv/firefox/webrender/WebRenderComponents.kt @@ -23,12 +23,12 @@ import org.mozilla.tv.firefox.utils.Settings */ class WebRenderComponents(applicationContext: Context, systemUserAgent: String) { // The first intent the App was launched with. Used to pass configuration through to Gecko. - lateinit var launchSafeIntent: SafeIntent + private var launchSafeIntent: SafeIntent? = null fun notifyLaunchWithSafeIntent(safeIntent: SafeIntent): Boolean { // We can't access the property reference outside of our own lexical scope, // so this helper must be in this class. - if (!this::launchSafeIntent.isInitialized) { + if (launchSafeIntent == null) { launchSafeIntent = safeIntent return true } @@ -45,7 +45,7 @@ class WebRenderComponents(applicationContext: Context, systemUserAgent: String) if (BuildConstants.isDevBuild) { // In debug builds, allow to invoke via an Intent that has extras customizing Gecko. // In particular, this allows to add command line arguments for custom profiles, etc. - val extras = launchSafeIntent.extras + val extras = launchSafeIntent?.extras if (extras != null) { runtimeSettingsBuilder.extras(extras) } diff --git a/app/src/main/java/org/mozilla/tv/firefox/IntentValidator.kt b/app/src/main/java/org/mozilla/tv/firefox/IntentValidator.kt index 092b1bced4..cb4de1c1b8 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/IntentValidator.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/IntentValidator.kt @@ -14,7 +14,6 @@ import mozilla.components.service.fretboard.ExperimentDescriptor import mozilla.components.support.utils.SafeIntent import org.mozilla.tv.firefox.components.locale.LocaleManager import org.mozilla.tv.firefox.ext.serviceLocator -import org.mozilla.tv.firefox.pocket.PocketVideoFetchScheduler import org.mozilla.tv.firefox.telemetry.TelemetryIntegration import org.mozilla.tv.firefox.utils.UrlUtils @@ -59,7 +58,6 @@ object IntentValidator { fun validate(context: Context, intent: SafeIntent): ValidatedIntentData? { setQAExperimentOverrides(intent, context) - setQAFetchDelayOverrides(intent, context.serviceLocator.pocketVideoFetchScheduler) setQALocaleOverride(intent, context) when (intent.action) { @@ -102,12 +100,6 @@ object IntentValidator { } } - private fun setQAFetchDelayOverrides(intent: SafeIntent, pocketVideoFetchScheduler: PocketVideoFetchScheduler) { - intent.extras?.getLong(EXTRA_FETCH_DELAY_KEY)?.let { delay -> - pocketVideoFetchScheduler.setQAFetchDelayOverride(delay) - } - } - private fun setQALocaleOverride(intent: SafeIntent, context: Context) { val selectedLocale = intent.extras?.getString(EXTRA_SELECTED_LOCALE) ?: return val localeManager = LocaleManager.getInstance() diff --git a/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt b/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt index 977c858d46..2bfb42592b 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt @@ -13,7 +13,6 @@ import org.mozilla.tv.firefox.hint.HintContentFactory import org.mozilla.tv.firefox.navigationoverlay.ChannelTitles import org.mozilla.tv.firefox.navigationoverlay.NavigationOverlayViewModel import org.mozilla.tv.firefox.navigationoverlay.OverlayHintViewModel -import org.mozilla.tv.firefox.pocket.PocketViewModel import org.mozilla.tv.firefox.navigationoverlay.ToolbarViewModel import org.mozilla.tv.firefox.settings.SettingsViewModel import org.mozilla.tv.firefox.utils.ServiceLocator @@ -40,11 +39,6 @@ class ViewModelFactory( @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { return when (modelClass) { - PocketViewModel::class.java -> PocketViewModel( - resources, - serviceLocator.pocketRepo - ) as T - ToolbarViewModel::class.java -> ToolbarViewModel( sessionRepo = serviceLocator.sessionRepo, pinnedTileRepo = serviceLocator.pinnedTileRepo diff --git a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelConfig.kt b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelConfig.kt index 0d987912eb..8c9e41c501 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelConfig.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelConfig.kt @@ -24,19 +24,6 @@ data class ChannelConfig( val enabledInLocales: KillswitchLocales ) { companion object { - fun getPocketConfig(context: Context): ChannelConfig = ChannelConfig( - onClickTelemetry = { tile -> - TELEMETRY.pocketVideoClickEvent(tile.id) - TELEMETRY.homeTileClickEvent(context, tile) - }, - // TODO focus telemetry should probably only be sent on focus gain, but this is - // how our previous implementation worked. Keeping this to maintain data consistency - onFocusTelemetry = { tile, _ -> TELEMETRY.pocketVideoImpressionEvent(tile.id) }, - isEnabledInCurrentExperiment = true, - // Pocket is enabled in all countries, for any English locale - enabledInLocales = KillswitchLocales.ActiveIn(Locale.ENGLISH) - ) - fun getPinnedTileConfig(context: Context): ChannelConfig = ChannelConfig( onClickTelemetry = { tile -> TELEMETRY.homeTileClickEvent(context, tile) }, itemsMayBeRemoved = true, diff --git a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelRepo.kt b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelRepo.kt index 734758ea85..6a33cc3628 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelRepo.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelRepo.kt @@ -69,7 +69,6 @@ class ChannelRepo( addBundleTileToBlackList(tileData.tileSource, tileData.id) pinnedTileRepo.removePinnedTile(tileData.url) } - TileSource.POCKET -> throw NotImplementedError("pocket shouldn't be able to remove tiles") TileSource.NEWS, TileSource.SPORTS, TileSource.MUSIC -> { addBundleTileToBlackList(tileData.tileSource, tileData.id) } diff --git a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelTile.kt b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelTile.kt index 9a1fa32bc6..35e24cbe2a 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelTile.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/channels/ChannelTile.kt @@ -19,7 +19,7 @@ import org.mozilla.tv.firefox.ext.getDimenPixelSize import org.mozilla.tv.firefox.utils.PicassoWrapper import java.io.File -enum class TileSource { BUNDLED, CUSTOM, POCKET, NEWS, SPORTS, MUSIC } +enum class TileSource { BUNDLED, CUSTOM, NEWS, SPORTS, MUSIC } /** * Backing data for a [RecyclerView] item in a channel @@ -42,7 +42,6 @@ data class ChannelTile( return when (tileSource) { TileSource.BUNDLED, TileSource.CUSTOM -> context.resources.getString(R.string.pinned_tiles_channel_remove_title, title) - TileSource.POCKET -> throw NotImplementedError("pocket shouldn't be able to remove tiles") TileSource.NEWS -> context.resources.getString(R.string.news_channel_remove_title, title) TileSource.SPORTS -> diff --git a/app/src/main/java/org/mozilla/tv/firefox/channels/DefaultChannelAdapter.kt b/app/src/main/java/org/mozilla/tv/firefox/channels/DefaultChannelAdapter.kt index a702446135..a5cb07b4c3 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/channels/DefaultChannelAdapter.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/channels/DefaultChannelAdapter.kt @@ -69,17 +69,7 @@ class DefaultChannelAdapter( val tile = getItem(position) tile.setImage.invoke(imageView) - // We handle Pocket tiles differently. - if (tile.subtitle != null) { - titleView.visibility = View.GONE - - pocketTitle.text = tile.subtitle - pocketTitle.visibility = View.VISIBLE - pocketAuthor.text = tile.title - pocketAuthor.visibility = View.VISIBLE - } else { - titleView.text = tile.title - } + titleView.text = tile.title itemView.setOnClickListener { loadUrl(tile.url) @@ -137,7 +127,5 @@ class DefaultChannelAdapter( class DefaultChannelTileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val titleView: TextView = itemView.findViewById(R.id.tile_title) - val pocketAuthor: TextView = itemView.findViewById(R.id.tile_pocket_author) - val pocketTitle: TextView = itemView.findViewById(R.id.tile_pocket_title) val imageView: ImageView = itemView.findViewById(R.id.tile_icon) } diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketEndpointRaw.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketEndpointRaw.kt deleted file mode 100644 index 9bff760fa2..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketEndpointRaw.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.net.Uri -import android.util.Log -import androidx.annotation.AnyThread -import okhttp3.Request -import org.mozilla.tv.firefox.BuildConfig -import org.mozilla.tv.firefox.ext.executeAndAwait -import org.mozilla.tv.firefox.utils.OkHttpWrapper -import java.io.IOException -import java.util.concurrent.TimeoutException - -private const val LOGTAG = "PocketEndpointRaw" - -/** Make requests to the Pocket endpoint and returns raw data: see [PocketEndpoint] for more. */ -@Deprecated("Move to android-components implementation #1976") -class PocketEndpointRaw( - private val appVersion: String, - private val pocketGlobalVideoEndpoint: Uri? -) { - - /** @return The global video recommendations as a raw JSON str or null on error. */ - @AnyThread // executeAndAwait hands off the request to the OkHttp dispatcher. - suspend fun getGlobalVideoRecommendations(): String? { - pocketGlobalVideoEndpoint ?: return null - - val req = Request.Builder() - .url(pocketGlobalVideoEndpoint.toString()) - .header("User-Agent", "FirefoxTV-$appVersion-${BuildConfig.BUILD_TYPE}") - .build() - - val res = try { - OkHttpWrapper.client.newCall(req).executeAndAwait() - } catch (e: IOException) { - Log.w(LOGTAG, "getGlobalVideoRecommendations: network error") - Log.w(LOGTAG, e) - return null - } catch (e: TimeoutException) { - Log.w(LOGTAG, "getGlobalVideoRecommendations: timed out") - Log.w(LOGTAG, e) - return null - } - - return if (res.isSuccessful) res.body()?.string() else null - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchScheduler.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchScheduler.kt deleted file mode 100644 index d5d2bf91a7..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchScheduler.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.annotation.VisibleForTesting.PRIVATE -import androidx.lifecycle.Lifecycle.Event.ON_START -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import java.util.Calendar -import java.util.concurrent.TimeUnit -import kotlin.random.Random - -private const val FETCH_UNIQUE_WORK_NAME = "PocketFetch" - -/** - * Schedules background fetches of the Pocket video data. - */ -class PocketVideoFetchScheduler( - private val isPocketEnabledByLocale: () -> Boolean, - private val context: Context -) : LifecycleObserver { - private var qaFetchDelayOverrideMillis: Long? = null - private var isQABuild = false - init { resetQAFetchDelayOverrides() } // Replace initial values above to avoid duplication. - - @OnLifecycleEvent(ON_START) - fun onStart() { - schedulePocketBackgroundFetch(WorkManager.getInstance(context)) - } - - @VisibleForTesting(otherwise = PRIVATE) - fun schedulePocketBackgroundFetch( - workManager: WorkManager, - now: Calendar = Calendar.getInstance(), - randLong: (Long, Long) -> Long = { from, until -> Random.nextLong(from, until) } - ) { - fun getBackoffDelayMillisWithRandomness(): Long { - return randLong(BACKOFF_DELAY_MIN_MILLIS, BACKOFF_DELAY_MAX_MILLIS) - } - - // This may not be the best place to early return based on locale - e.g. it duplicates state with the UI - - // but we're short on time. - if (!isPocketEnabledByLocale()) { - return - } - - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - // This sets the custom fetch delay for testing purposes. - // The custom delay will only run once. - val delay: Long - val workPolicy: ExistingWorkPolicy - if (isQABuild) { - delay = checkNotNull(qaFetchDelayOverrideMillis) { "Fetch delay value must be set" } - workPolicy = ExistingWorkPolicy.REPLACE - resetQAFetchDelayOverrides() - } else { - delay = getDelayUntilUpcomingNightFetchMillis(now, randLong) - workPolicy = ExistingWorkPolicy.KEEP - } - - // When the user foregrounds the app, we schedule a one time update for a random time inside our fetch interval - // (currently overnight). This will ensure that users always see fresh content every morning and that the content - // is only refreshing at times when the user is most likely not using the app. - // The request will always be scheduled for the next night, so if the user foregrounds the app after - // midnight but before the fetch interval, we schedule for the following night. - // We only schedule a fetch if there isn't one already scheduled. - val saveRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setInitialDelay(delay, TimeUnit.MILLISECONDS) - - // Here exponential means the first backoff is the given delay, the second backoff is the given delay * 2, - // the third backoff is the given delay * 2 * 2, etc. Note that WorkManager does not introduce randomness - // in its backoff algorithm so we must supply our own. This prevents the case where many clients hit the - // server all at once, overloading the server, then they all back-off and make new requests all at the same - // time, again overloading the server, and repeat. - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, getBackoffDelayMillisWithRandomness(), TimeUnit.MILLISECONDS) - .build() - - workManager.enqueueUniqueWork(FETCH_UNIQUE_WORK_NAME, workPolicy, saveRequest) - } - - private fun getDelayUntilUpcomingNightFetchMillis( - now: Calendar, - randLong: (Long, Long) -> Long - ): Long { - val nextFetchIntervalStartTime = now.cloneCalendar().apply { - set(Calendar.HOUR_OF_DAY, FETCH_START_HOUR) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - - // A time after midnight is one calendar day after now. - add(Calendar.DATE, 1) - } - - val fetchOffsetSeconds = randLong(0L, FETCH_INTERVAL_DURATION_SECONDS).toInt() - val userFetchTime = nextFetchIntervalStartTime.cloneCalendar().apply { - add(Calendar.SECOND, fetchOffsetSeconds) - } - - return userFetchTime.timeInMillis - now.timeInMillis - } - - /** - * Sets the fetch delay to a specified number of seconds. This makes it easier to manually test. - */ - fun setQAFetchDelayOverride(seconds: Long) { - isQABuild = true - qaFetchDelayOverrideMillis = TimeUnit.SECONDS.toMillis(seconds) - } - - /** - * Resets the fetch delay values to production values. We do this so we do not spam the Pocket servers. - */ - private fun resetQAFetchDelayOverrides() { - isQABuild = false - qaFetchDelayOverrideMillis = null - } - - companion object { - @VisibleForTesting(otherwise = PRIVATE) const val FETCH_START_HOUR = 3 // am - @VisibleForTesting(otherwise = PRIVATE) const val FETCH_END_HOUR = 5L - private val FETCH_INTERVAL_DURATION_SECONDS = - TimeUnit.HOURS.toSeconds(FETCH_END_HOUR - FETCH_START_HOUR) - - // Since this is a background job, we're in no rush and can wait a while. - @VisibleForTesting(otherwise = PRIVATE) val BACKOFF_DELAY_MIN_MILLIS = TimeUnit.SECONDS.toMillis(30) - @VisibleForTesting(otherwise = PRIVATE) val BACKOFF_DELAY_MAX_MILLIS = TimeUnit.SECONDS.toMillis(60) - } -} - -// We keep this private because we don't know if cloning Calendars handles all the edge cases. -private fun Calendar.cloneCalendar(): Calendar = clone() as Calendar diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorker.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorker.kt deleted file mode 100644 index 9bd3212899..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorker.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import kotlinx.coroutines.runBlocking -import org.mozilla.tv.firefox.ext.serviceLocator - -/** - * A background task that fetches the Pocket video recommendations and stores them to disk, if valid. - * - * IMPORTANT: to avoid overloading the server, job requests using this worker are expected to: - * - Avoid scheduling jobs for many clients at the same time (e.g. all clients at refresh at 3am) - * - Add randomness to their setBackoffCriteria timing (for errors) - */ -class PocketVideoFetchWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { - - private val pocketEndpointRaw = appContext.serviceLocator.pocketEndpointRaw - private val store = appContext.serviceLocator.pocketVideoStore - - override fun doWork(): Result { - val rawJSONStr = runBlocking { pocketEndpointRaw.getGlobalVideoRecommendations() } - if (rawJSONStr == null) { - // If the connection to the server failed, we should try to reconnect. Beyond typical - // scheduling delays, WorkManager does not add randomness to this retry logic: as - // mentioned in the class kdoc, job requests are expected to add their own randomness. - return Result.retry() - } - - val wasSaveSuccessful = store.save(rawJSONStr) - if (!wasSaveSuccessful) { - // We only can't save if the JSON is invalid so the server returned invalid JSON. - // The server is probably in a bad state: we should wait before connecting again. - return Result.failure() - } - - return Result.success() - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoParser.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoParser.kt deleted file mode 100644 index 97491ada51..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoParser.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.util.Log -import org.json.JSONException -import org.json.JSONObject -import org.mozilla.tv.firefox.ext.flatMapObj - -private const val LOGTAG = "PocketVideoParser" - -/** - * Handles marshalling [PocketViewModel.FeedItem.Video] objects from JSON. - */ -@Deprecated("Move to android-components implementation #1976") -object PocketVideoParser { - - // Ideally, this functionality would be in a separate class but 1) we're short on time and 2) this - // functionality should be handled by the a-c implementation in the long term. - /** @return The videos or null on error; the list will never be empty. */ - fun convertVideosJSON(jsonStr: String): List? = try { - val rawJSON = JSONObject(jsonStr) - val videosJSON = rawJSON.getJSONArray("recommendations") - val videos = videosJSON.flatMapObj { PocketViewModel.FeedItem.Video.fromJSONObject(it) } - if (videos.isNotEmpty()) videos else null - } catch (e: JSONException) { - Log.w(LOGTAG, "convertVideosJSON: invalid JSON from Pocket server") - Log.w(LOGTAG, e) - null - } - - fun parse(jsonObj: JSONObject): PocketViewModel.FeedItem.Video? = try { - PocketViewModel.FeedItem.Video( - id = jsonObj.getInt("id"), - title = jsonObj.getString("title"), - // Note that the 'url' property of our domain object can be retrieved from - // either of two JSON fields, and we make no distinction internally - url = jsonObj.optString("tv_url", null) ?: jsonObj.getString("url"), - thumbnailURL = jsonObj.getString("image_src"), - popularitySortId = jsonObj.getInt("popularity_sort_id"), - authors = getAuthorName(jsonObj) - ) - } catch (e: JSONException) { - null - } - - private fun getAuthorName(jsonObj: JSONObject): String { - return try { - val authors = mutableListOf() - val authorsJSON = JSONObject(jsonObj.getString("authors")) - // TODO: verify if multiple authors are possible and what the format of authors object is - for (x in authorsJSON.keys()) { - val authorObject = JSONObject(authorsJSON[x].toString()) - authors += authorObject.getString("name") - } - authors.joinToString() - } catch (e: JSONException) { - "" - } - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoRepo.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoRepo.kt deleted file mode 100644 index b15fd346ab..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoRepo.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import androidx.annotation.UiThread -import androidx.annotation.VisibleForTesting -import androidx.annotation.VisibleForTesting.PRIVATE -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.subjects.BehaviorSubject -import org.mozilla.tv.firefox.pocket.PocketVideoRepo.Companion.newInstance - -/** - * Manages backing state for Pocket data, as well as any logic related to - * retrieving or storing that data. - * - * To get an instance, use [newInstance] rather than the constructor. - */ -open class PocketVideoRepo @VisibleForTesting(otherwise = PRIVATE) constructor( - private val pocketVideoStore: PocketVideoStore, - private val isPocketEnabledByLocale: () -> Boolean, - private val _feedState: BehaviorSubject -) { - - sealed class FeedState { - data class LoadComplete(val videos: List) : FeedState() - object NoAPIKey : FeedState() - object Inactive : FeedState() - } - - open val feedState = _feedState.hide() - .observeOn(AndroidSchedulers.mainThread()) - .distinctUntilChanged() // avoid churn because we may retrieve similar results in onStart. - - /** - * Gets the latest Pocket videos from the store and pushes it to the UI. This is intentionally - * not reactive because a reactive update model might replace the content when the user is looking - * at it. This method is expected to be called when the user is not looking at the content (e.g. in onStart). - */ - @UiThread // not sure if this annotation is necessary anymore. - fun updatePocketFromStore() { - // If we have no API key, this will always be the state: there is nothing to do. In theory, now that we - // ship with content, we could remove the NoAPIKey UI state but then it'd be less obvious if we accidentally - // shipped a release build without a key: we should make that decision separately from this PR. - if (_feedState.value == FeedState.NoAPIKey) { - return - } - - if (!isPocketEnabledByLocale()) { - _feedState.onNext(FeedState.Inactive) - return - } - - val videos = pocketVideoStore.load() - _feedState.onNext(FeedState.LoadComplete(videos)) - } - - companion object { - /** - * Returns a new [PocketVideoRepo]: this is the preferred way to construct an instance. - */ - fun newInstance( - videoStore: PocketVideoStore, - isPocketEnabledByLocale: () -> Boolean, - isPocketKeyValid: Boolean - ): PocketVideoRepo { - val feedState = BehaviorSubject.createDefault(if (!isPocketKeyValid) FeedState.NoAPIKey else FeedState.Inactive) - return PocketVideoRepo(videoStore, isPocketEnabledByLocale, feedState) - } - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoStore.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoStore.kt deleted file mode 100644 index c24c7373d1..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketVideoStore.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.Context -import android.content.res.AssetManager -import android.util.Log -import androidx.annotation.AnyThread -import androidx.annotation.VisibleForTesting -import androidx.annotation.VisibleForTesting.PRIVATE -import org.mozilla.tv.firefox.telemetry.SentryIntegration - -private const val LOGTAG = "PocketVideoStore" - -private const val VIDEO_STORE_NAME = "Pocket-Global-Video-Recs" - -private const val BUNDLED_VIDEOS_PATH = "bundled/pocket_videos.json" - -/** - * Saves the Pocket video recommendations as a raw JSON String and loads them in data structures for the app. - * Bad data should not be saved so bad data should never be returned. - * - * This class is thread safe. - */ -class PocketVideoStore( - appContext: Context, - private val assets: AssetManager, - private val jsonValidator: PocketVideoJSONValidator -) { - - // We use SharedPrefs because it's simple, it handles concurrency (so we don't even need to think about - // which threads we access this from), and the file sizes will be small. - private val sharedPrefs = appContext.getSharedPreferences(VIDEO_STORE_NAME, 0) - - /** - * @return true if the save was successful, false if the save was not successful because the JSON is invalid. - */ - @AnyThread - fun save(json: String): Boolean { - if (!jsonValidator.isJSONValidForSaving(json)) { - return false - } - - sharedPrefs.edit() - .putString(KEY_VIDEO_JSON, json) - .apply() - - return true - } - - /** - * @return the list of loaded videos. This should never happen but in case of error, the empty list is returned. - */ - @AnyThread - fun load(): List { - fun loadBundledTiles(): String = assets.open(BUNDLED_VIDEOS_PATH).bufferedReader().use { it.readText() } - - val rawJSON = sharedPrefs.getString(KEY_VIDEO_JSON, null) ?: loadBundledTiles() - - val convertedVideos = jsonValidator.convertJSONToPocketVideos(rawJSON) - if (convertedVideos == null) { - // We don't expect the conversion to ever fail: we only save valid JSON and we fallback to the presumably - // valid bundled content if we've never saved. We don't crash because it may cause an infinite crash loop - // for users but we record the error to Sentry. Users will see an undefined state: presumably the Pocket - // channel will be empty but who knows if focusing around it will work correctly. - Log.e(LOGTAG, "Error converting JSON to Pocket video") - SentryIntegration.capture(IllegalStateException("Error converting JSON to Pocket video")) - return emptyList() - } - - return convertedVideos - } - - companion object { - @VisibleForTesting(otherwise = PRIVATE) const val KEY_VIDEO_JSON = "video_json" - } -} - -/** - * Validates video recommendation json from the Pocket server. - */ -class PocketVideoJSONValidator( - @Suppress("DEPRECATION") // We need PocketVideoParser until we move to a-c's impl. - private val pocketVideoParser: PocketVideoParser -) { - fun isJSONValidForSaving(rawJSON: String): Boolean { - // While we don't need the conversion result, this function already handles validation so we use - // it to validate the videos. - val convertedVideos = pocketVideoParser.convertVideosJSON(rawJSON) - return convertedVideos != null && - - // Guarantee a minimum number of Pocket videos: e.g. if the server only returns one valid video, - // we wouldn't want to overwrite what the user already has to show only one video. - convertedVideos.size >= REQUIRED_POCKET_VIDEO_COUNT - } - - // Using a function reference causes typing problems so we wrap this in a function call instead. - fun convertJSONToPocketVideos(json: String) = pocketVideoParser.convertVideosJSON(json) - - companion object { - /** - * The minimum number of valid videos we must receive from the server if we want to display them. - * This number is set to the number of videos that appear on screen at the time of writing. - */ - @VisibleForTesting(otherwise = PRIVATE) const val REQUIRED_POCKET_VIDEO_COUNT = 4 - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketViewModel.kt b/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketViewModel.kt deleted file mode 100644 index ac3cbe866e..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/pocket/PocketViewModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.res.Resources -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.ViewModel -import io.reactivex.Observable -import org.json.JSONObject -import org.mozilla.tv.firefox.R -import org.mozilla.tv.firefox.channels.ChannelDetails -import org.mozilla.tv.firefox.channels.ChannelTile -import org.mozilla.tv.firefox.channels.ImageSetStrategy -import org.mozilla.tv.firefox.channels.TileSource - -const val POCKET_VIDEO_COUNT = 20 -const val PLACEHOLDER_IMAGE = "https://blog.mozilla.org/firefox/files/2017/12/Screen-Shot-2017-12-18-at-2.39.25-PM.png" - -/** - * Provides data that maps 1:1 to Pocket view state. - * - * This view state is provided by transforming backing state (provided by the - * [PocketVideoRepo]), stripping information not important to the view, and adding - * information required by the view. This should be enough to render (i.e., the - * view should not have to perform any transformations on this data). - */ -class PocketViewModel( - resources: Resources, - pocketRepo: PocketVideoRepo -) : ViewModel() { - - private val pocketTitle = resources.getString(R.string.pocket_channel_title2) - private val pocketSubtitle = resources.getString(R.string.pocket_channel_subtitle) - - sealed class State { - data class Feed(val details: ChannelDetails) : State() - object NotDisplayed : State() - } - - sealed class FeedItem { - data class Video( - val id: Int, - val title: String, - val url: String, - val thumbnailURL: String, - val popularitySortId: Int, - val authors: String - ) : FeedItem() { - companion object { - @Suppress("DEPRECATION") // We need PocketVideoParser until we move to a-c's impl. - fun fromJSONObject(jsonObject: JSONObject) = PocketVideoParser.parse(jsonObject) - } - } - } - - val state: Observable = pocketRepo.feedState - .map { repoState -> - when (repoState) { - is PocketVideoRepo.FeedState.LoadComplete -> State.Feed(repoState.videos.toChannelDetails()) - is PocketVideoRepo.FeedState.NoAPIKey -> State.Feed(noKeyPlaceholders) - is PocketVideoRepo.FeedState.Inactive -> State.NotDisplayed - } - } - .replay(1) - .autoConnect(0) - - private fun List.toChannelDetails(): ChannelDetails = ChannelDetails( - title = pocketTitle, - subtitle = pocketSubtitle, - tileList = this.toChannelTiles() - ) - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val noKeyPlaceholders: ChannelDetails by lazy { - ChannelDetails( - title = pocketTitle, - subtitle = pocketSubtitle, - tileList = List(POCKET_VIDEO_COUNT) { - ChannelTile( - url = "https://www.mozilla.org/en-US/", - title = "Mozilla", - subtitle = "Mozilla", - setImage = ImageSetStrategy.ByPath(PLACEHOLDER_IMAGE), - tileSource = TileSource.POCKET, - id = it.toString() - ) - } - ) - } -} - -@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun List.toChannelTiles() = this.map { when (it) { - is PocketViewModel.FeedItem.Video -> ChannelTile( - url = it.url, - title = it.authors, - subtitle = it.title, - setImage = ImageSetStrategy.ByPath(it.thumbnailURL), - tileSource = TileSource.POCKET, - id = it.id.toString() - ) -} } diff --git a/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryFactory.kt b/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryFactory.kt index 44ed1e8f61..d26b741e53 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryFactory.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryFactory.kt @@ -14,7 +14,6 @@ import org.mozilla.telemetry.measurement.DefaultSearchMeasurement import org.mozilla.telemetry.net.TelemetryClient import org.mozilla.telemetry.ping.TelemetryCorePingBuilder import org.mozilla.telemetry.ping.TelemetryMobileEventPingBuilder -import org.mozilla.telemetry.ping.TelemetryPocketEventPingBuilder import org.mozilla.telemetry.schedule.jobscheduler.JobSchedulerTelemetryScheduler import org.mozilla.telemetry.serialize.JSONPingSerializer import org.mozilla.telemetry.storage.FileTelemetryStorage @@ -60,7 +59,6 @@ object TelemetryFactory { return Telemetry(configuration, storage, telemetryClient, scheduler) .addPingBuilder(TelemetryCorePingBuilder(configuration)) .addPingBuilder(TelemetryMobileEventPingBuilder(configuration)) - .addPingBuilder(TelemetryPocketEventPingBuilder(configuration)) .setDefaultSearchProvider(createDefaultSearchProvider(context)) } diff --git a/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryIntegration.kt b/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryIntegration.kt index 67fa9b6ed9..35a461e811 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryIntegration.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/telemetry/TelemetryIntegration.kt @@ -10,7 +10,6 @@ import android.net.http.SslError import android.os.StrictMode import android.view.InputDevice import android.view.KeyEvent -import androidx.annotation.AnyThread import androidx.annotation.UiThread import mozilla.components.concept.sync.DeviceType import mozilla.components.support.ktx.android.os.resetAfter @@ -18,7 +17,6 @@ import org.mozilla.telemetry.event.TelemetryEvent import org.mozilla.telemetry.measurement.SearchesMeasurement import org.mozilla.telemetry.ping.TelemetryCorePingBuilder import org.mozilla.telemetry.ping.TelemetryMobileEventPingBuilder -import org.mozilla.telemetry.ping.TelemetryPocketEventPingBuilder import org.mozilla.tv.firefox.channels.ChannelTile import org.mozilla.tv.firefox.channels.SettingsButton import org.mozilla.tv.firefox.channels.SettingsScreen @@ -29,7 +27,6 @@ import org.mozilla.tv.firefox.fxa.FxaReceivedTab import org.mozilla.tv.firefox.navigationoverlay.NavigationEvent import org.mozilla.tv.firefox.utils.Assert import org.mozilla.tv.firefox.widget.InlineAutocompleteEditText.AutocompleteResult -import java.util.Collections private const val SHARED_PREFS_KEY = "telemetryLib" // Don't call it TelemetryWrapper to avoid accidental IDE rename. private const val KEY_CLICKED_HOME_TILE_IDS_PER_SESSION = "clickedHomeTileIDsPerSession" @@ -53,7 +50,6 @@ open class TelemetryIntegration protected constructor( const val ACTION = "action" const val AGGREGATE = "aggregate" const val ERROR = "error" - const val POCKET = "pocket" } private object Method { @@ -85,7 +81,6 @@ open class TelemetryIntegration protected constructor( const val HOME_TILE = "home_tile" const val TURBO_MODE = "turbo_mode" const val PIN_PAGE = "pin_page" - const val POCKET_VIDEO = "pocket_video" const val MEDIA_SESSION = "media_session" const val DESKTOP_MODE = "desktop_mode" const val VIDEO_ID = "video_id" @@ -103,7 +98,6 @@ open class TelemetryIntegration protected constructor( const val OFF = "off" const val TILE_BUNDLED = "bundled" const val TILE_CUSTOM = "custom" - const val TILE_POCKET = "pocket" const val YOUTUBE_TILE = "youtube_tile" const val EXIT_FIREFOX = "exit" const val SETTINGS_CLEAR_DATA_TILE = "clear_data_tile" @@ -137,10 +131,6 @@ open class TelemetryIntegration protected constructor( const val BOOLEAN = "boolean" } - // Available on any thread: we synchronize. - private val pocketUniqueClickedVideoIDs = Collections.synchronizedSet(mutableSetOf()) - private val pocketUniqueImpressedVideoIDs = Collections.synchronizedSet(mutableSetOf()) - fun init(context: Context) { // When initializing the telemetry library it will make sure that all directories exist and // are readable/writable. @@ -177,37 +167,19 @@ open class TelemetryIntegration protected constructor( resetSessionMeasurements(context) } - // EXT to add events to pocket ping (independently from mobile_events) - fun TelemetryEvent.queueInPocketPing() { - if (!DeprecatedTelemetryHolder.get().configuration.isCollectionEnabled) { - return - } - - (DeprecatedTelemetryHolder.get() - .getPingBuilder(TelemetryPocketEventPingBuilder.TYPE) as TelemetryPocketEventPingBuilder) - .eventsMeasurement.add(this) - } - private fun queueSessionMeasurements(context: Context) { TelemetryHomeTileUniqueClickPerSessionCounter.queueEvent(context) - TelemetryEvent.create(Category.AGGREGATE, Method.CLICK, Object.POCKET_VIDEO, - pocketUniqueClickedVideoIDs.size.toString()).queue() - queuePocketVideoImpressionEvent() - queuePocketVideoClickEvent() } private fun resetSessionMeasurements(context: Context) { TelemetryHomeTileUniqueClickPerSessionCounter.resetSessionData(context) TelemetryRemoteControlTracker.resetSessionData(context) - pocketUniqueClickedVideoIDs.clear() - pocketUniqueImpressedVideoIDs.clear() } fun stopMainActivity() { DeprecatedTelemetryHolder.get() .queuePing(TelemetryCorePingBuilder.TYPE) .queuePing(TelemetryMobileEventPingBuilder.TYPE) - .queuePing(TelemetryPocketEventPingBuilder.TYPE) .scheduleUpload() } @@ -420,30 +392,6 @@ open class TelemetryIntegration protected constructor( getTileTypeAsStringValue(removedTile)).queue() } - @AnyThread // pocketUniqueClickedVideoIDs is synchronized. - fun pocketVideoClickEvent(id: String) { - pocketUniqueClickedVideoIDs.add(id) - } - - @AnyThread // pocketUniqueImpressVideoIDs is synchronized. - fun pocketVideoImpressionEvent(id: String) { - pocketUniqueImpressedVideoIDs.add(id) - } - - private fun queuePocketVideoImpressionEvent() { - for (videoId in pocketUniqueImpressedVideoIDs) { - TelemetryEvent.create(Category.POCKET, Method.IMPRESSION, Object.VIDEO_ID, - videoId).queueInPocketPing() - } - } - - private fun queuePocketVideoClickEvent() { - for (videoId in pocketUniqueClickedVideoIDs) { - TelemetryEvent.create(Category.POCKET, Method.CLICK, Object.VIDEO_ID, - videoId.toString()).queueInPocketPing() - } - } - fun mediaSessionEvent(eventType: MediaSessionEventType) { val method = when (eventType) { MediaSessionEventType.PLAY_PAUSE_BUTTON -> Method.CLICK @@ -457,7 +405,6 @@ open class TelemetryIntegration protected constructor( private fun getTileTypeAsStringValue(tile: ChannelTile) = when (tile.tileSource) { TileSource.BUNDLED -> Value.TILE_BUNDLED TileSource.CUSTOM -> Value.TILE_CUSTOM - TileSource.POCKET -> Value.TILE_POCKET TileSource.NEWS -> Value.TILE_BUNDLED TileSource.SPORTS -> Value.TILE_BUNDLED TileSource.MUSIC -> Value.TILE_BUNDLED diff --git a/app/src/main/java/org/mozilla/tv/firefox/utils/BuildConfigDerivables.kt b/app/src/main/java/org/mozilla/tv/firefox/utils/BuildConfigDerivables.kt deleted file mode 100644 index ab325e3682..0000000000 --- a/app/src/main/java/org/mozilla/tv/firefox/utils/BuildConfigDerivables.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.utils - -import android.net.Uri -import org.mozilla.tv.firefox.BuildConfig - -private const val POCKET_PARAM_API_KEY = "consumer_key" - -/** - * Computes information that must be derived from the [BuildConfig]. - * - * This logic is often simple but noisy, so pulling it out of client code improves readability. - */ -class BuildConfigDerivables(pocketKey: String? = BuildConfig.POCKET_KEY) { - val isPocketKeyValid = !pocketKey.isNullOrBlank() - - // Pocket key can be null if it was not included in the build. In this case we - // know that any calls to Pocket will fail, and so we do not provide the - // endpoint at all to prevent unnecessary requests. - @Suppress("UselessCallOnNotNull") - val globalPocketVideoEndpoint: Uri? = when { - BuildConfig.POCKET_KEY.isNullOrEmpty() -> null - else -> Uri.parse("https://getpocket.cdn.mozilla.net/v3/firefox/global-video-recs") - .buildUpon() - .appendQueryParameter(POCKET_PARAM_API_KEY, BuildConfig.POCKET_KEY) - .appendQueryParameter("version", "2") - .appendQueryParameter("authors", "1") - .build() - } -} diff --git a/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt b/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt index 7d201e9001..f5a2e6d96a 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt @@ -17,7 +17,6 @@ import org.mozilla.tv.firefox.architecture.ViewModelFactory import org.mozilla.tv.firefox.channels.ChannelRepo import org.mozilla.tv.firefox.channels.pinnedtile.PinnedTileImageUtilWrapper import org.mozilla.tv.firefox.channels.pinnedtile.PinnedTileRepo -import org.mozilla.tv.firefox.components.locale.LocaleManager import org.mozilla.tv.firefox.experiments.ExperimentsProvider import org.mozilla.tv.firefox.experiments.FretboardProvider import org.mozilla.tv.firefox.ext.getAccessibilityManager @@ -26,12 +25,6 @@ import org.mozilla.tv.firefox.framework.FrameworkRepo import org.mozilla.tv.firefox.fxa.ADMIntegration import org.mozilla.tv.firefox.fxa.FxaLoginUseCase import org.mozilla.tv.firefox.fxa.FxaRepo -import org.mozilla.tv.firefox.pocket.PocketEndpointRaw -import org.mozilla.tv.firefox.pocket.PocketVideoFetchScheduler -import org.mozilla.tv.firefox.pocket.PocketVideoJSONValidator -import org.mozilla.tv.firefox.pocket.PocketVideoParser -import org.mozilla.tv.firefox.pocket.PocketVideoRepo -import org.mozilla.tv.firefox.pocket.PocketVideoStore import org.mozilla.tv.firefox.search.SearchEngineManagerFactory import org.mozilla.tv.firefox.session.SessionRepo import org.mozilla.tv.firefox.settings.SettingsRepo @@ -71,10 +64,6 @@ import org.mozilla.tv.firefox.webrender.cursor.CursorModel */ open class ServiceLocator(val app: Application) { private val appVersion = app.packageManager.getPackageInfo(app.packageName, 0).versionName - private val buildConfigDerivables get() = BuildConfigDerivables() - private val isPocketEnabledByLocale = { LocaleManager.getInstance().currentLanguageIsEnglish(app) } // Pocket is en-US only. - private val pocketVideoParser by lazy { PocketVideoParser } - private val pocketVideoJSONValidator by lazy { PocketVideoJSONValidator(pocketVideoParser) } val intentLiveData by lazy { MutableLiveData>() } val fretboardProvider: FretboardProvider by lazy { FretboardProvider(app) } @@ -90,10 +79,6 @@ open class ServiceLocator(val app: Application) { val screenshotStoreWrapper by lazy { PinnedTileImageUtilWrapper(app) } val formattedDomainWrapper by lazy { FormattedDomainWrapper(app) } val channelRepo by lazy { ChannelRepo(app, screenshotStoreWrapper, formattedDomainWrapper, pinnedTileRepo) } - @Suppress("DEPRECATION") // We need PocketEndpointRaw until we move to a-c's impl. - val pocketEndpointRaw by lazy { PocketEndpointRaw(appVersion, buildConfigDerivables.globalPocketVideoEndpoint) } - val pocketVideoStore by lazy { PocketVideoStore(app, app.assets, pocketVideoJSONValidator) } - val pocketVideoFetchScheduler by lazy { PocketVideoFetchScheduler(isPocketEnabledByLocale, app.applicationContext) } val fxaRepo by lazy { FxaRepo(app, admIntegration = admIntegration) } val fxaLoginUseCase by lazy { FxaLoginUseCase(fxaRepo, sessionRepo, screenController) } val admIntegration by lazy { ADMIntegration(app) } @@ -102,11 +87,6 @@ open class ServiceLocator(val app: Application) { // These open vals are overridden in testing open val frameworkRepo = FrameworkRepo.newInstanceAndInit(app.getAccessibilityManager()) open val pinnedTileRepo by lazy { PinnedTileRepo(app) } - open val pocketRepo by lazy { PocketVideoRepo.newInstance( - pocketVideoStore, - isPocketEnabledByLocale, - buildConfigDerivables.isPocketKeyValid - ) } open val sessionRepo by lazy { SessionRepo(sessionManager, sessionUseCases, turboMode).apply { observeSources() } } open val settingsRepo by lazy { SettingsRepo(app) } } diff --git a/app/src/main/java/org/mozilla/tv/firefox/utils/URLs.kt b/app/src/main/java/org/mozilla/tv/firefox/utils/URLs.kt index ce492d0015..d918660d5e 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/utils/URLs.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/utils/URLs.kt @@ -10,7 +10,6 @@ package org.mozilla.tv.firefox.utils object URLs { private const val APP_URL_PREFIX = "firefox:" const val APP_URL_HOME = "${APP_URL_PREFIX}home" - const val APP_URL_POCKET_ERROR = "${APP_URL_PREFIX}error:pocketconnection" const val FIREFOX_ACCOUNTS = "https://accounts.firefox.com" diff --git a/app/src/main/java/org/mozilla/tv/firefox/webrender/CustomContentRequestInterceptor.kt b/app/src/main/java/org/mozilla/tv/firefox/webrender/CustomContentRequestInterceptor.kt index 017d308818..dfa325e332 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/webrender/CustomContentRequestInterceptor.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/webrender/CustomContentRequestInterceptor.kt @@ -31,7 +31,7 @@ class CustomContentRequestInterceptor( currentPageURL = uri return when (uri) { - URLs.APP_URL_HOME, URLs.APP_URL_POCKET_ERROR -> + URLs.APP_URL_HOME -> RequestInterceptor.InterceptionResponse.Content("") URLs.URL_ABOUT -> getInterceptionResponseContent( diff --git a/app/src/main/res/layout/home_tile.xml b/app/src/main/res/layout/home_tile.xml index d8901a532e..c5833b2ab8 100644 --- a/app/src/main/res/layout/home_tile.xml +++ b/app/src/main/res/layout/home_tile.xml @@ -48,33 +48,5 @@ android:textColor="@color/photonGrey10_a80p" android:textSize="18sp" tools:text="YouTube" /> - - - - diff --git a/app/src/test/java/org/mozilla/tv/firefox/architecture/FirefoxViewModelProvidersTest.kt b/app/src/test/java/org/mozilla/tv/firefox/architecture/FirefoxViewModelProvidersTest.kt index 3422bb2048..962a0b1e7f 100644 --- a/app/src/test/java/org/mozilla/tv/firefox/architecture/FirefoxViewModelProvidersTest.kt +++ b/app/src/test/java/org/mozilla/tv/firefox/architecture/FirefoxViewModelProvidersTest.kt @@ -10,9 +10,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.tv.firefox.MainActivity -import org.mozilla.tv.firefox.pocket.PocketViewModel import org.robolectric.Robolectric import org.mozilla.tv.firefox.helpers.FirefoxRobolectricTestRunner +import org.mozilla.tv.firefox.navigationoverlay.ToolbarViewModel @RunWith(FirefoxRobolectricTestRunner::class) class FirefoxViewModelProvidersTest { @@ -32,7 +32,7 @@ class FirefoxViewModelProvidersTest { @Test fun `WHEN passed a valid ViewModel THEN of(FragmentActivity) returns a non-null value`() { - assertNotNull(FirefoxViewModelProviders.of(mainActivity).get(PocketViewModel::class.java)) + assertNotNull(FirefoxViewModelProviders.of(mainActivity).get(ToolbarViewModel::class.java)) } } diff --git a/app/src/test/java/org/mozilla/tv/firefox/helpers/PocketTestData.kt b/app/src/test/java/org/mozilla/tv/firefox/helpers/PocketTestData.kt deleted file mode 100644 index 4619d05586..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/helpers/PocketTestData.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.helpers - -import org.mozilla.tv.firefox.pocket.PocketViewModel - -/** - * A collection of functions that generate test data for Pocket-related code. - */ -object PocketTestData { - fun getVideoItem(id: Int) = - PocketViewModel.FeedItem.Video( - id = id, - title = "Mozilla", - url = "https://www.mozilla.org/en-US/", - thumbnailURL = "https://blog.mozilla.org/firefox/files/2017/12/Screen-Shot-2017-12-18-at-2.39.25-PM.png", - popularitySortId = id, - authors = "0:{'name':'Mozilla'}" - ) - - fun getVideoFeed(size: Int) = List(size) { getVideoItem(it) } -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/navigationoverlay/OverlayHintViewModelTest.kt b/app/src/test/java/org/mozilla/tv/firefox/navigationoverlay/OverlayHintViewModelTest.kt index 60dfd5972d..15b3be0783 100644 --- a/app/src/test/java/org/mozilla/tv/firefox/navigationoverlay/OverlayHintViewModelTest.kt +++ b/app/src/test/java/org/mozilla/tv/firefox/navigationoverlay/OverlayHintViewModelTest.kt @@ -49,7 +49,6 @@ class OverlayHintViewModelTest { sessionRepoState.onNext(fakeSessionState(url = "https://www.mozilla.org")) sessionRepoState.onNext(fakeSessionState(url = "https://www.google.com")) sessionRepoState.onNext(fakeSessionState(url = URLs.PRIVACY_NOTICE_URL)) - sessionRepoState.onNext(fakeSessionState(url = URLs.APP_URL_POCKET_ERROR)) sessionRepoState.onNext(fakeSessionState(url = URLs.URL_LICENSES)) hints.values().forEach { diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchSchedulerTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchSchedulerTest.kt deleted file mode 100644 index 0357b59224..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchSchedulerTest.kt +++ /dev/null @@ -1,155 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import io.mockk.MockKAnnotations -import io.mockk.called -import io.mockk.impl.annotations.MockK -import io.mockk.slot -import io.mockk.spyk -import io.mockk.verify -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.tv.firefox.pocket.PocketVideoFetchScheduler.Companion.FETCH_END_HOUR -import org.mozilla.tv.firefox.pocket.PocketVideoFetchScheduler.Companion.FETCH_START_HOUR -import org.mozilla.tv.firefox.helpers.FirefoxRobolectricTestRunner -import java.util.Calendar - -@RunWith(FirefoxRobolectricTestRunner::class) -class PocketVideoFetchSchedulerTest { - - private lateinit var scheduler: PocketVideoFetchScheduler - @MockK(relaxed = true) private lateinit var workManager: WorkManager - - private var isPocketEnabledByLocale: Boolean = true - - @Before - fun setUp() { - MockKAnnotations.init(this) - scheduler = PocketVideoFetchScheduler({ isPocketEnabledByLocale }, context = testContext) - isPocketEnabledByLocale = true - } - - @Test - fun `GIVEN fetch interval constants THEN start hour is less than end hour`() { - assertTrue(FETCH_START_HOUR < PocketVideoFetchScheduler.FETCH_END_HOUR) - } - - @Test - fun `GIVEN backoff interval constants THEN min is less than max`() { - assertTrue(PocketVideoFetchScheduler.BACKOFF_DELAY_MIN_MILLIS < PocketVideoFetchScheduler.BACKOFF_DELAY_MAX_MILLIS) - } - - @Test - fun `WHEN onStart is called THEN schedulePocketBackgroundFetch is called`() { - // This test is tightly coupled to the implementation but, given this daily background job will rarely be - // tested, we want to verify the implementation hasn't accidentally changed in a way that broke the functionality. - val schedulerMock = spyk(PocketVideoFetchScheduler({ true }, testContext)) - schedulerMock.onStart() - verify(exactly = 1) { schedulerMock.schedulePocketBackgroundFetch(any(), any(), any()) } - } - - @Test - fun `GIVEN Pocket is disabled by locale THEN WorkManager is never interacted with so a job is never queued`() { - isPocketEnabledByLocale = false - scheduler.schedulePocketBackgroundFetch(workManager) - verify { workManager wasNot called } - } - - @Test - fun `GIVEN Pocket is enabled by locale THEN WorkManager will enqueue a request with a connected NetworkType`() { - scheduler.schedulePocketBackgroundFetch(workManager) - val request = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(NetworkType.CONNECTED, request.workSpec.constraints.requiredNetworkType) - } - - @Test - fun `GIVEN Pocket is enabled by locale THEN WorkManager will enqueue a request using the keep existing work policy`() { - scheduler.schedulePocketBackgroundFetch(workManager) - val policy = slot() - verify { workManager.enqueueUniqueWork(any(), capture(policy), any() as OneTimeWorkRequest) } - assertEquals(ExistingWorkPolicy.KEEP, policy.captured) - } - - @Test - fun `GIVEN Pocket is enabled by locale THEN WorkManager will enqueue a request with an exponential backoff criteria`() { - scheduler.schedulePocketBackgroundFetch(workManager) - val request = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(BackoffPolicy.EXPONENTIAL, request.workSpec.backoffPolicy) - } - - @Test - fun `GIVEN Pocket is enabled by locale THEN WorkManager will enqueue a request with a backoff delay based on randomness`() { - // For these tests, we assume the returned value from the random number generator is used directly. - var fromArg = 0L - scheduler.schedulePocketBackgroundFetch(workManager, randLong = { from, _ -> from.also { fromArg = from } }) - val request = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(fromArg, request.workSpec.backoffDelayDuration) - - var untilArg = 0L - scheduler.schedulePocketBackgroundFetch(workManager, randLong = { _, until -> until.also { untilArg = until } }) - val request2 = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(untilArg, request2.workSpec.backoffDelayDuration) - - assertNotEquals(fromArg, untilArg) // Sanity check to verify test correctness on the tricky capturing above. - } - - @Test - fun `GIVEN there is no randomness WHEN scheduling a pocket background fetch THEN the initial delay is the millis until the expected fetch time`() { - val now = Calendar.getInstance().apply { - set(1, 1, 1, 2, 30, 30) - set(Calendar.MILLISECOND, 500) - } - val expectedFetchTimeForNoRandomness = Calendar.getInstance().apply { - set(1, 1, 2, FETCH_START_HOUR, 0, 0) - set(Calendar.MILLISECOND, 0) - } - - // We'd return zero but randLong is used somewhere else and then we'd need robolectric. - // Implicit assumption that 0 is minimum for this. - val randLong = { from: Long, _: Long -> from } - val expected = expectedFetchTimeForNoRandomness.timeInMillis - now.timeInMillis - - scheduler.schedulePocketBackgroundFetch(workManager, now, randLong) - - val request = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(expected, request.workSpec.initialDelay) - } - - @Test - fun `GIVEN there is randomness WHEN scheduling a pocket background fetch THEN the initial delay is the millis until the expected fetch time plus random millis`() { - val now = Calendar.getInstance().apply { - set(1, 1, 1, 2, 30, 30) - set(Calendar.MILLISECOND, 500) - } - val nextFetchIntervalEndTime = Calendar.getInstance().apply { - set(1, 1, 2, FETCH_END_HOUR.toInt(), 0, 0) - set(Calendar.MILLISECOND, 0) - } - - val randLong = { _: Long, until: Long -> until } - val expected = nextFetchIntervalEndTime.timeInMillis - now.timeInMillis - - scheduler.schedulePocketBackgroundFetch(workManager, now, randLong) - - val request = verifyWorkManagerEnqueueAndCaptureRequest() - assertEquals(expected, request.workSpec.initialDelay) - } - - private fun verifyWorkManagerEnqueueAndCaptureRequest(): OneTimeWorkRequest = slot().also { - verify { workManager.enqueueUniqueWork(any(), any(), capture(it)) } - assertTrue(it.isCaptured) - }.captured -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorkerTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorkerTest.kt deleted file mode 100644 index 1e28da84e5..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoFetchWorkerTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import androidx.work.ListenableWorker.Result -import androidx.work.WorkerParameters -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.mozilla.tv.firefox.FirefoxApplication -import org.mozilla.tv.firefox.utils.ServiceLocator - -class PocketVideoFetchWorkerTest { - - private lateinit var worker: PocketVideoFetchWorker - - @MockK private lateinit var context: FirefoxApplication - @Suppress("DEPRECATION") // We need endpoint raw until we move to the a-c impl. - @MockK private lateinit var endpointRaw: PocketEndpointRaw - @MockK private lateinit var store: PocketVideoStore - - @MockK private lateinit var workerParams: WorkerParameters - - @Before - fun setUp() { - MockKAnnotations.init(this) - - every { context.applicationContext } returns context - every { context.serviceLocator } returns mockk().also { - every { it.pocketEndpointRaw } returns endpointRaw - every { it.pocketVideoStore } returns store - } - - worker = PocketVideoFetchWorker(context, workerParams) - } - - @Test - fun `WHEN the endpoint fails to return a result THEN the job will retry`() { - coEvery { endpointRaw.getGlobalVideoRecommendations() } returns null - assertEquals(Result.retry(), worker.doWork()) - } - - @Test - fun `WHEN the endpoint returns a non-null result THEN the store decides what happens to the exact json string`() { - arrayOf( - "", - "{", - "{}", - "{\"json\": \"yeah!\"}" - ).forEachIndexed { i, json -> - coEvery { endpointRaw.getGlobalVideoRecommendations() } returns json - every { store.save(any()) } returns true - - worker.doWork() - - println("For index $i: $json") - verify { store.save(json) } - } - } - - @Test - fun `WHEN the store says the endpoint returned invalid json THEN the job will fail`() { - // The store returns false if the JSON is invalid and refuses to be saved: - // unfortunately, this naming is not very intuitive. - everyEndpointRawGetVideoRecsReturnsANonNullValue() - every { store.save(any()) } returns false - assertEquals(Result.failure(), worker.doWork()) - } - - @Test - fun `WHEN the endpoint returns valid json THEN the job will succeed`() { - // The store returns true if the JSON is valid and can be saved: - // unfortunately, this naming is not very intuitive. - everyEndpointRawGetVideoRecsReturnsANonNullValue() - every { store.save(any()) } returns true - assertEquals(Result.success(), worker.doWork()) - } - - private fun everyEndpointRawGetVideoRecsReturnsANonNullValue() { - // For many tests, we only need a non-null return value to advance to the next portion of the function, - // which is the part we want to test. - coEvery { endpointRaw.getGlobalVideoRecommendations() } returns "{}" - } -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoJSONValidatorTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoJSONValidatorTest.kt deleted file mode 100644 index d384077340..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoJSONValidatorTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.slot -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mozilla.tv.firefox.helpers.PocketTestData - -class PocketVideoJSONValidatorTest { - - @Suppress("DEPRECATION") - @MockK private lateinit var pocketVideoParser: PocketVideoParser - - private lateinit var validator: PocketVideoJSONValidator - - @Before - fun setup() { - MockKAnnotations.init(this) - - validator = PocketVideoJSONValidator(pocketVideoParser) - } - - @Test - fun `WHEN validating for saving THEN the json argument is passed verbatim into the parser convert function`() { - val json = "{ }" - val slot = slot() - every { pocketVideoParser.convertVideosJSON(capture(slot)) } returns null - validator.isJSONValidForSaving(json) - - assertEquals(json, slot.captured) - } - - @Test - fun `WHEN converting json to pocket videos THEN it delegates to the pocket parser`() { - val json = "{ }" - val slot = slot() - every { pocketVideoParser.convertVideosJSON(capture(slot)) } returns null - validator.convertJSONToPocketVideos(json) - - assertEquals(json, slot.captured) - } - - @Test - fun `WHEN validating for saving and converted videos is null THEN return false`() { - every { pocketVideoParser.convertVideosJSON(any()) } returns null - assertFalse(validator.isJSONValidForSaving("{ }")) - } - - @Test - fun `WHEN validating for saving and converted videos size is equal to required video count THEN return true`() { - every { pocketVideoParser.convertVideosJSON(any()) } returns - PocketTestData.getVideoFeed(PocketVideoJSONValidator.REQUIRED_POCKET_VIDEO_COUNT) - assertTrue(validator.isJSONValidForSaving("{ }")) - } - - @Test - fun `WHEN validating for saving and converted videos size is less than required video count THEN return false`() { - every { pocketVideoParser.convertVideosJSON(any()) } returns - PocketTestData.getVideoFeed(PocketVideoJSONValidator.REQUIRED_POCKET_VIDEO_COUNT - 1) - assertFalse(validator.isJSONValidForSaving("{ }")) - } -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoParserTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoParserTest.kt deleted file mode 100644 index 5b46576254..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoParserTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -@file:Suppress("DEPRECATION") // The class under test is deprecated. - -package org.mozilla.tv.firefox.pocket - -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.fail -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.tv.firefox.helpers.TestResource -import org.mozilla.tv.firefox.helpers.FirefoxRobolectricTestRunner - -private const val KEY_INNER = "recommendations" - -@RunWith(FirefoxRobolectricTestRunner::class) -class PocketVideoParserTest { - - private val pocketVideoParser = PocketVideoParser - - @Test - fun `convert Videos JSON successfully to internal objects`() { - val expectedSubset = listOf( - PocketViewModel.FeedItem.Video( - id = 27587, - title = "How a Master Pastry Chef Uses Architecture to Make Sky High Pastries", - url = "https://www.youtube.com/tv#/watch/video/idle?v=953Qt4FnAcU", - thumbnailURL = "https://img-getpocket.cdn.mozilla.net/direct?url=http%3A%2F%2Fimg.youtube.com%2Fvi%2F953Qt4FnAcU%2Fmaxresdefault.jpg&resize=w450", - popularitySortId = 20, - authors = "Eater" - ), - PocketViewModel.FeedItem.Video( - id = 27581, - title = "How Does Having Too Much Power Affect Your Brain?", - url = "https://www.youtube.com/watch?v=GHZ7-kq3GDQ", - thumbnailURL = "https://img-getpocket.cdn.mozilla.net/direct?url=http%3A%2F%2Fimg.youtube.com%2Fvi%2FGHZ7-kq3GDQ%2Fmaxresdefault.jpg&resize=w450", - popularitySortId = 17, - // TODO: Update this to be the platform, once the behaviour is fixed (issue #1484) - authors = "" - ) - ) - - val pocketJSON = TestResource.POCKET_VIDEO_RECOMMENDATION.get() - val actualVideos = pocketVideoParser.convertVideosJSON(pocketJSON) - if (actualVideos == null) { fail("Expected actualVideos to be non-null"); return } - - // We only test a subset of the data for developer sanity. :) - assertEquals(20, actualVideos.size) - expectedSubset.forEachIndexed { i, expected -> - assertEquals(expected, actualVideos[i]) - } - } - - @Test - fun `convert Videos JSON for videos with missing fields drops those items`() { - val pocketJSON = TestResource.POCKET_VIDEO_RECOMMENDATION.get() - - val expectedFirstTitle = JSONObject(pocketJSON).getJSONArray(KEY_INNER).getJSONObject(0).getString("title") - assertNotNull(expectedFirstTitle) - - val pocketJSONWithNoTitleExceptFirst = removeTitleStartingAtIndex(1, pocketJSON) - val actualVideos = pocketVideoParser.convertVideosJSON(pocketJSONWithNoTitleExceptFirst) - if (actualVideos == null) { fail("Expected videos non-null"); return } - assertEquals(1, actualVideos.size) - assertEquals(expectedFirstTitle, actualVideos[0].title) - } - - @Test - fun `convert Videos JSON for videos with missing fields on all items`() { - val pocketJSON = TestResource.POCKET_VIDEO_RECOMMENDATION.get() - val pocketJSONWithNoTitles = removeTitleStartingAtIndex(0, pocketJSON) - val actualVideos = pocketVideoParser.convertVideosJSON(pocketJSONWithNoTitles) - assertNull(actualVideos) - } - - @Test - fun `convert Videos JSON for empty String`() { - assertNull(pocketVideoParser.convertVideosJSON("")) - } - - @Test - fun `convert Videos JSON for invalid JSON`() { - assertNull(pocketVideoParser.convertVideosJSON("{!!}}")) - } -} - -private fun removeTitleStartingAtIndex(startIndex: Int, json: String): String { - val obj = JSONObject(json) - val videosJson = obj.getJSONArray(KEY_INNER) - for (i in startIndex until videosJson.length()) { - videosJson.getJSONObject(i).remove("title") - } - return obj.toString() -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoRepoTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoRepoTest.kt deleted file mode 100644 index 61dcbbc93a..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoRepoTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.reactivex.observers.TestObserver -import io.reactivex.subjects.BehaviorSubject -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.tv.firefox.helpers.PocketTestData -import org.mozilla.tv.firefox.helpers.RxTestHelper -import org.mozilla.tv.firefox.pocket.PocketVideoRepo.FeedState -import org.mozilla.tv.firefox.helpers.FirefoxRobolectricTestRunner - -@RunWith(FirefoxRobolectricTestRunner::class) -class PocketVideoRepoTest { - - companion object { - @BeforeClass @JvmStatic - fun beforeClass() { - RxTestHelper.forceRxSynchronousInBeforeClass() - } - } - - private lateinit var repo: PocketVideoRepo - @MockK private lateinit var videoStore: PocketVideoStore - private var isPocketEnabledByLocale = false - - private lateinit var feedState: BehaviorSubject - private lateinit var feedStateTestObs: TestObserver - - @Before - fun setUp() { - MockKAnnotations.init(this) - feedState = BehaviorSubject.create() - feedStateTestObs = feedState.test() - repo = PocketVideoRepo(videoStore, { isPocketEnabledByLocale }, feedState) - } - - @Test - fun `GIVEN feedState is no api key WHEN updating Pocket from store THEN feedState is still no api key`() { - feedState.onNext(FeedState.NoAPIKey) - - repo.updatePocketFromStore() - - feedStateTestObs.assertValues(FeedState.NoAPIKey) - } - - @Test - fun `GIVEN pocket is not enabled by locale WHEN updating Pocket from store THEN feedState becomes inactive`() { - repo.updatePocketFromStore() - - feedStateTestObs.assertValues(FeedState.Inactive) - } - - @Test - fun `GIVEN pocket is enabled by locale WHEN updating Pocket from store THEN feedState has videos loaded by store`() { - isPocketEnabledByLocale = true - val videos = PocketTestData.getVideoFeed(3) - every { videoStore.load() } returns videos - repo.updatePocketFromStore() - - feedStateTestObs.assertValues(FeedState.LoadComplete(videos)) - } - - @Test - fun `GIVEN an existing feed state WHEN updating Pocket from store THEN feedState can change between the expected values`() { - val videos = PocketTestData.getVideoFeed(3) - every { videoStore.load() } returns videos - feedState.onNext(FeedState.Inactive) - - isPocketEnabledByLocale = true - repo.updatePocketFromStore() - - isPocketEnabledByLocale = false - repo.updatePocketFromStore() - - isPocketEnabledByLocale = true - repo.updatePocketFromStore() - - feedStateTestObs.assertValues( - FeedState.Inactive, - FeedState.LoadComplete(videos), - FeedState.Inactive, - FeedState.LoadComplete(videos) - ) - } - - @Test // sanity check - fun `WHEN getting a new instance THEN no exception is thrown`() { - PocketVideoRepo.newInstance(videoStore, { true }, true) - } - - @Test - fun `GIVEN pocket key is valid WHEN getting a new instance THEN feed state starts inactive`() { - // This test uses different instances than the ones created in the @Before block - val repo = PocketVideoRepo.newInstance(videoStore, { true }, isPocketKeyValid = true) - repo.feedState.test().assertValue(FeedState.Inactive) - } - - @Test - fun `GIVEN pocket key is not valid WHEN getting a new instance THEN feed state starts no api key`() { - // This test uses different instances than the ones created in the @Before block - val repo = PocketVideoRepo.newInstance(videoStore, { true }, isPocketKeyValid = false) - repo.feedState.test().assertValue(FeedState.NoAPIKey) - } -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoStoreTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoStoreTest.kt deleted file mode 100644 index 886fce2f88..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketVideoStoreTest.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.Context -import android.content.SharedPreferences -import android.content.res.AssetManager -import androidx.test.core.app.ApplicationProvider -import io.mockk.CapturingSlot -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.slot -import io.mockk.spyk -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.tv.firefox.helpers.PocketTestData -import org.mozilla.tv.firefox.helpers.FirefoxRobolectricTestRunner -import java.io.ByteArrayInputStream -import java.io.IOException -import java.io.InputStream - -private val POCKET_FEED_TEST_DATA = PocketTestData.getVideoFeed(1) - -@RunWith(FirefoxRobolectricTestRunner::class) -class PocketVideoStoreTest { - private lateinit var pocketVideoStore: PocketVideoStore - private lateinit var sharedPrefs: SharedPreferences - - @MockK private lateinit var context: Context - @MockK private lateinit var assetManager: AssetManager - @MockK private lateinit var jsonValidator: PocketVideoJSONValidator - - @Before - fun setup() { - MockKAnnotations.init(this) - - sharedPrefs = ApplicationProvider.getApplicationContext().getSharedPreferences("PocketFetch", 0) - every { context.getSharedPreferences(any(), any()) } returns sharedPrefs - - pocketVideoStore = PocketVideoStore(context, assetManager, jsonValidator) - } - - @Test - fun `WHEN the raw json is valid THEN it is saved in shared preferences`() { - every { jsonValidator.isJSONValidForSaving(any()) } returns true - val expected = "{ }" - - assertTrue(pocketVideoStore.save(expected)) - - val actual = sharedPrefs.getString("video_json", null) - - assertEquals(expected, actual) - } - - @Test - fun `WHEN the raw json is invalid THEN it is not saved in shared preferences`() { - every { jsonValidator.isJSONValidForSaving(any()) } returns false - - assertFalse(pocketVideoStore.save("{ }")) - } - - @Test - fun `WHEN saving THEN the json to save is passed verbatim into the json validator`() { - arrayOf("{ }", "", " ", "{ ").forEachIndexed { i, json -> - // The return value doesn't matter. - val validatorJSONCaptured = slot() - every { jsonValidator.isJSONValidForSaving(capture(validatorJSONCaptured)) } returns true - - pocketVideoStore.save(json) - - println("Input index $i: $json") - assertEquals(json, validatorJSONCaptured.captured) - } - } - - @Test - fun `GIVEN shared preferences is empty WHEN loading THEN the bundled tiles json is passed verbatim into the json validator`() { - val expectedBundledJSON = "assets" - everyAssetManagerOpensInputStream(expectedBundledJSON.toInputStream()) - - // The return value doesn't matter. - val validatorJSONCaptured = everyJSONValidatorConversionReturnsList(emptyList()) - - pocketVideoStore.load() - - assertEquals(expectedBundledJSON, validatorJSONCaptured.captured) - } - - // shared pref videos is copied into validator - @Test - fun `GIVEN shared preferences is not empty WHEN loading THEN the json from shared prefs is passed verbatim into the json validator`() { - everyAssetManagerOpensInputStream("assets".toInputStream()) - val expectedSharedPrefsValue = "{ }" - setJSONInSharedPrefs(expectedSharedPrefsValue) - - // The return value doesn't matter. - val validatorJSONCaptured = everyJSONValidatorConversionReturnsList(emptyList()) - - pocketVideoStore.load() - - assertEquals(expectedSharedPrefsValue, validatorJSONCaptured.captured) - } - - @Test - fun `GIVEN shared preferences is not empty WHEN loading THEN the converted videos are returned`() { - everyAssetManagerOpensInputStream("assets".toInputStream()) - setJSONInSharedPrefs("{ }") - everyJSONValidatorConversionReturnsList(POCKET_FEED_TEST_DATA) - - val returnVal = pocketVideoStore.load() - - assertEquals(POCKET_FEED_TEST_DATA, returnVal) - } - - @Test - fun `GIVEN shared preferences is empty WHEN the converted videos is null THEN loading returns an empty list`() { - everyAssetManagerOpensInputStream("".toInputStream()) - everyJSONValidatorConversionReturnsList(null) - val returnVal = pocketVideoStore.load() - assertEquals(emptyList(), returnVal) - } - - @Test - fun `GIVEN shared preferences is empty and conversion returns null WHEN loading THEN the input stream gets closed`() { - verifyLoadInputStreamIsClosed(given = { - everyJSONValidatorConversionReturnsList(null) - }) - } - - @Test - fun `GIVEN shared preferences is empty and conversion does not return null WHEN loading THEN the input stream gets closed`() { - verifyLoadInputStreamIsClosed(given = { - everyJSONValidatorConversionReturnsList(POCKET_FEED_TEST_DATA) - }) - } - - @Test(expected = IOException::class) - fun `GIVEN shared preferences is empty WHEN opening asset manager throws an exception THEN load throws the exception`() { - every { assetManager.open(any()) } throws IOException() - pocketVideoStore.load() - } - - @Test(expected = IOException::class) - fun `GIVEN shared preferences is empty WHEN read text throws an exception THEN load throws the exception`() { - val inputStream = mockk().also { - every { it.read(any(), any(), any()) } throws IOException() - } - - everyAssetManagerOpensInputStream(inputStream) - pocketVideoStore.load() - } - - private fun verifyLoadInputStreamIsClosed(given: (InputStream) -> Unit) { - val inputStream = spyk("assets".toInputStream()) - everyAssetManagerOpensInputStream(inputStream) - given(inputStream) - - pocketVideoStore.load() - - verify { inputStream.close() } - } - - private fun setJSONInSharedPrefs(json: String) { - sharedPrefs.edit().putString(PocketVideoStore.KEY_VIDEO_JSON, json).apply() - } - - private fun everyAssetManagerOpensInputStream(inputStream: InputStream) { - every { assetManager.open(any()) } returns inputStream - } - - private fun everyJSONValidatorConversionReturnsList(pocketVideoList: List?): CapturingSlot { - val validatorJSONCaptured = slot() - every { jsonValidator.convertJSONToPocketVideos(capture(validatorJSONCaptured)) } returns pocketVideoList - return validatorJSONCaptured - } - - private fun String.toInputStream() = ByteArrayInputStream(this.toByteArray()) -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketViewModelTest.kt b/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketViewModelTest.kt deleted file mode 100644 index 08af3e1aad..0000000000 --- a/app/src/test/java/org/mozilla/tv/firefox/pocket/PocketViewModelTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.tv.firefox.pocket - -import android.content.res.Resources -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.reactivex.observers.TestObserver -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.mozilla.tv.firefox.R -import org.mozilla.tv.firefox.channels.ChannelDetails - -private const val POCKET_TITLE = "Pocket Title" -private const val POCKET_SUBTITLE = "Pocket Subtitle" - -class PocketViewModelTest { - - @MockK lateinit var resources: Resources - - private lateinit var viewModel: PocketViewModel - private lateinit var repoCacheState: Subject - private lateinit var testObserver: TestObserver - - private lateinit var noKeyPlaceholders: ChannelDetails - - @Before - fun setup() { - MockKAnnotations.init(this) - every { resources.getString(R.string.pocket_channel_title2) } answers { POCKET_TITLE } - every { resources.getString(R.string.pocket_channel_subtitle) } answers { POCKET_SUBTITLE } - - repoCacheState = PublishSubject.create() - val repo = mock(PocketVideoRepo::class.java) - `when`(repo.feedState).thenReturn(repoCacheState) - - viewModel = PocketViewModel(resources, repo) - noKeyPlaceholders = viewModel.noKeyPlaceholders - testObserver = viewModel.state.test() - } - - @Test - fun `WHEN repo cache emits a successful fetch THEN view model should emit a feed with the same videos`() { - val videos = listOf( - PocketViewModel.FeedItem.Video(1, "", "", "", 1, "") - ) - val expected = ChannelDetails( - title = POCKET_TITLE, - subtitle = POCKET_SUBTITLE, - tileList = videos.toChannelTiles() - ) - - repoCacheState.onNext(PocketVideoRepo.FeedState.LoadComplete(videos)) - - assertEquals(1, testObserver.valueCount()) - - testObserver.values()[0].let { - assertTrue(it is PocketViewModel.State.Feed) - assertEquals(expected, (it as PocketViewModel.State.Feed).details) - } - } - - @Test - fun `WHEN repo cache emits no key THEN view model should emit a feed of no key placeholders`() { - repoCacheState.onNext(PocketVideoRepo.FeedState.NoAPIKey) - - assertEquals(1, testObserver.valueCount()) - - testObserver.values()[0].let { - assertTrue(it is PocketViewModel.State.Feed) - assertEquals(noKeyPlaceholders, (it as PocketViewModel.State.Feed).details) - } - } - - @Test - fun `WHEN repo cache emits inactive THEN view model should emit not displayed`() { - repoCacheState.onNext(PocketVideoRepo.FeedState.Inactive) - - assertEquals(1, testObserver.valueCount()) - - assertEquals(PocketViewModel.State.NotDisplayed, testObserver.values()[0]) - } -} diff --git a/app/src/test/java/org/mozilla/tv/firefox/webrender/WebRenderHintViewModelTest.kt b/app/src/test/java/org/mozilla/tv/firefox/webrender/WebRenderHintViewModelTest.kt index 35e58a59ed..70c51a3454 100644 --- a/app/src/test/java/org/mozilla/tv/firefox/webrender/WebRenderHintViewModelTest.kt +++ b/app/src/test/java/org/mozilla/tv/firefox/webrender/WebRenderHintViewModelTest.kt @@ -62,7 +62,6 @@ class WebRenderHintViewModelTest { sessionRepoState.onNext(fakeSessionState(URLs.PRIVACY_NOTICE_URL)) pushCursorMove(Direction.LEFT) pushScrolledToEdge(Direction.UP) - sessionRepoState.onNext(fakeSessionState(URLs.APP_URL_POCKET_ERROR)) pushCursorMove(Direction.LEFT) sessionRepoState.onNext(fakeSessionState(URLs.URL_LICENSES))