Skip to content

Commit

Permalink
fixup! feat: use engage-sdk for continue watching
Browse files Browse the repository at this point in the history
  • Loading branch information
vighnesh153 committed Jan 16, 2024
1 parent bfd47cf commit c679632
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.reference.watchnext

import android.net.Uri
import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms
import com.android.tv.reference.shared.datamodel.Video
import com.android.tv.reference.shared.datamodel.VideoType
import com.android.tv.reference.watchnext.EngageWatchNextService.Companion.WatchNextVideo
import com.google.android.engage.video.datamodel.MovieEntity
import com.google.android.engage.video.datamodel.TvEpisodeEntity
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class EngageEntityConverterTest {
@Test
fun shouldConvertVideoToTvEpisodeEntity() {
val entity = VideoToEngageEntityConverter.convertVideo(
WatchNextVideo(
video = episodeVideo,
watchPosition = 100,
watchNextType = WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE,
)
) as TvEpisodeEntity

assertEquals(entity.entityId, VIDEO_ID)
assertEquals(entity.name, VIDEO_NAME)
assertEquals(entity.infoPageUri.toString(), URI.toString())
assertEquals(entity.playBackUri.toString(), VIDEO_URI.toString())
assertEquals(entity.posterImages.size, 1)
assertEquals(entity.posterImages[0].imageUri.toString(), THUMBNAIL_URI.toString())
assertEquals(entity.durationMillis, EPISODE_DURATION)
assertEquals(entity.episodeDisplayNumber, EPISODE_NUMBER)
assertEquals(entity.seasonNumber, SEASON_NUMBER)
}

@Test
fun shouldConvertVideoToMovieEntity() {
val entity = VideoToEngageEntityConverter.convertVideo(
WatchNextVideo(
video = movieVideo,
watchPosition = 100,
watchNextType = WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE,
)
) as MovieEntity

assertEquals(entity.entityId, VIDEO_ID)
assertEquals(entity.name, VIDEO_NAME)
assertEquals(entity.infoPageUri.toString(), URI.toString())
assertEquals(entity.playBackUri.toString(), VIDEO_URI.toString())
assertEquals(entity.posterImages.size, 1)
assertEquals(entity.posterImages[0].imageUri.toString(), THUMBNAIL_URI.toString())
assertEquals(entity.durationMillis, MOVIE_DURATION)
}

companion object {
private const val VIDEO_ID = "video-id"
private const val VIDEO_NAME = "video-name"
private const val VIDEO_DESCRIPTION = "video-description"
private val URI = Uri.parse("https://google.com/uri")
private val VIDEO_URI = Uri.parse("https://google.com/video.mp4")
private val THUMBNAIL_URI = Uri.parse("https://google.com/thumbnail.mp4")
private const val VIDEO_CATEGORY = "video-category"

private const val MOVIE_DURATION = "PT02H35M"

private const val EPISODE_DURATION = "PT00H45M"
private const val EPISODE_NUMBER = "02"
private const val SEASON_NUMBER = "01"

private val episodeVideo = Video(
id = VIDEO_ID,
name = VIDEO_NAME,
description = VIDEO_DESCRIPTION,
uri = URI.toString(),
videoUri = VIDEO_URI.toString(),
thumbnailUri = THUMBNAIL_URI.toString(),
backgroundImageUri = THUMBNAIL_URI.toString(),
category = VIDEO_CATEGORY,
videoType = VideoType.EPISODE,
duration = EPISODE_DURATION,
episodeNumber = EPISODE_NUMBER,
seasonNumber = SEASON_NUMBER,
)

private val movieVideo = Video(
id = VIDEO_ID,
name = VIDEO_NAME,
description = VIDEO_DESCRIPTION,
uri = URI.toString(),
videoUri = VIDEO_URI.toString(),
thumbnailUri = THUMBNAIL_URI.toString(),
backgroundImageUri = THUMBNAIL_URI.toString(),
category = VIDEO_CATEGORY,
videoType = VideoType.MOVIE,
duration = MOVIE_DURATION,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.reference.watchnext

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker
import androidx.work.ListenableWorker.Result
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.testing.TestListenableWorkerBuilder
import androidx.work.workDataOf
import com.android.tv.reference.watchnext.Constants.MAX_PUBLISHING_ATTEMPTS
import com.android.tv.reference.watchnext.Constants.PUBLISH_TYPE
import com.android.tv.reference.watchnext.Constants.PUBLISH_TYPE_CONTINUATION
import com.google.android.engage.service.AppEngageErrorCode
import com.google.android.engage.service.AppEngageException
import com.google.android.engage.service.AppEngagePublishClient
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.verify

class EngageServiceWorkerTest {

@Before
fun setUp() {
mockedContext = ApplicationProvider.getApplicationContext()
}

@Test
fun publishContinuationFailsWhenServiceUnavailableTest() {
val mockedAvailability = Tasks.forResult(false)
Mockito.`when`(mockedClient.isServiceAvailable()).thenReturn(mockedAvailability)

val mockedWorker =
createEngageServiceWorker(mockedContext, PUBLISH_TYPE_CONTINUATION, runAttempts = 0)
runBlocking {
val resultFail = mockedWorker.doWork()
assertEquals(Result.failure(), resultFail)
verify(mockedClient, Mockito.never()).publishContinuationCluster(any())
verify(mockedClient, Mockito.never()).deleteContinuationCluster()
verify(mockedClient, Mockito.never()).updatePublishStatus(any())
}
}

@Test
fun publishContinuationFailsOnUnrecoverableExceptions() {
val unrecoverableErrorCodes =
listOf(
AppEngageErrorCode.SERVICE_NOT_FOUND,
AppEngageErrorCode.SERVICE_NOT_AVAILABLE,
AppEngageErrorCode.SERVICE_CALL_INVALID_ARGUMENT,
AppEngageErrorCode.SERVICE_CALL_PERMISSION_DENIED,
)
for (errorCode in unrecoverableErrorCodes) {
verifyPublishContinuationWithErrorReturnsResultHelper(errorCode, Result.failure())
}
}

@Test
fun publishContinuationRetryOnRecoverableExceptions() {
val unrecoverableErrorCodes =
listOf(
AppEngageErrorCode.SERVICE_CALL_RESOURCE_EXHAUSTED,
AppEngageErrorCode.SERVICE_CALL_EXECUTION_FAILURE,
AppEngageErrorCode.SERVICE_CALL_INTERNAL
)
for (errorCode in unrecoverableErrorCodes) {
verifyPublishContinuationWithErrorReturnsResultHelper(errorCode, Result.retry())
}
}

@Test
fun attemptToPublishContinuationAtMaxAttemptsTest() {
val mockedAvailability = Tasks.forResult(true)
Mockito.`when`(mockedClient.isServiceAvailable()).thenReturn(mockedAvailability)

val resultingTask: Task<Void> = Tasks.forResult(null)

Mockito.`when`(mockedClient.publishContinuationCluster(any())).thenReturn(resultingTask)
Mockito.`when`(mockedClient.updatePublishStatus(any())).thenReturn(resultingTask)
// At least one movie is in progress

val worker =
createEngageServiceWorker(
mockedContext,
PUBLISH_TYPE_CONTINUATION,
MAX_PUBLISHING_ATTEMPTS
)

runBlocking {
worker.doWork()
verify { mockedClient.publishContinuationCluster(any()) }
}
}

@Test
fun doNotAttemptToPublishOrDeleteContinuationPastMaxAttemptsTest() {
val mockedAvailability = Tasks.forResult(true)
Mockito.`when`(mockedClient.isServiceAvailable()).thenReturn(mockedAvailability)

val worker =
createEngageServiceWorker(
mockedContext,
PUBLISH_TYPE_CONTINUATION,
MAX_PUBLISHING_ATTEMPTS + 1
)

runBlocking {
worker.doWork()
verify(mockedClient, Mockito.never()).publishContinuationCluster(any())
verify(mockedClient, Mockito.never()).deleteContinuationCluster()
}
}

private fun verifyPublishContinuationWithErrorReturnsResultHelper(
errorCode: Int,
expectedResult: Result
) {
val mockedAvailability = Tasks.forResult(true)
Mockito.`when`(mockedClient.isServiceAvailable).thenReturn(mockedAvailability)

val resultException = AppEngageException(errorCode)
val resultingTask: Task<Void> = Tasks.forException(resultException)

Mockito.`when`(mockedClient.publishContinuationCluster(any())).thenReturn(resultingTask)
Mockito.`when`(mockedClient.updatePublishStatus(any())).thenReturn(Tasks.forResult(null))
// At least one movie is in progress

val worker =
createEngageServiceWorker(mockedContext, PUBLISH_TYPE_CONTINUATION, runAttempts = 0)

runBlocking {
val actualResult = worker.doWork()
assertEquals(expectedResult, actualResult)
}
}

private fun createEngageServiceWorker(
context: Context,
publishClusterType: String,
runAttempts: Int
): EngageServiceWorker {
val workerData = workDataOf(PUBLISH_TYPE to publishClusterType)
return TestListenableWorkerBuilder<EngageServiceWorker>(
context = context,
inputData = workerData,
runAttemptCount = runAttempts
)
.setWorkerFactory(EngageServiceWorkerFactory())
.build()
}

private class EngageServiceWorkerFactory() : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker {
return EngageServiceWorker(appContext, workerParameters, mockedClient)
}
}

companion object {
private lateinit var mockedContext: Context
private val mockedClient: AppEngagePublishClient =
Mockito.mock(AppEngagePublishClient::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.reference.watchnext

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.android.tv.reference.watchnext.Constants.PERIODIC_WORKER_NAME_CONTINUATION
import com.android.tv.reference.watchnext.Constants.WORKER_NAME_CONTINUATION
import com.android.tv.reference.watchnext.Publisher.publishContinuationClusters
import com.android.tv.reference.watchnext.Publisher.publishPeriodically
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PublisherTest {

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
val config =
Configuration.Builder()
.setExecutor(SynchronousExecutor())
.setTaskExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
workManager = WorkManager.getInstance(context)
}

@Test
fun publishContinuationStartsWorkTest() {
publishContinuationClusters(context)
assertSetStateWorkIsQueuedHelper(WORKER_NAME_CONTINUATION)
}

@Test
fun publishPeriodicWorkersTest() {
publishPeriodically(context)
assertSetStateWorkIsQueuedHelper(PERIODIC_WORKER_NAME_CONTINUATION)
}

private fun assertSetStateWorkIsQueuedHelper(workName: String) {
val workInfo = workManager.getWorkInfosForUniqueWork(workName).get()
// This should always be true since publishing work is unique and non-chainable
assertTrue(workInfo.size == 0 || workInfo.size == 1)

// Work info will only be present if the work was triggered.
val hasStarted = workInfo.size == 1
assertTrue(hasStarted)
}

private companion object {
lateinit var workManager: WorkManager
lateinit var context: Context
}
}

0 comments on commit c679632

Please sign in to comment.