-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixup! feat: use engage-sdk for continue watching
- Loading branch information
1 parent
bfd47cf
commit c679632
Showing
3 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
113 changes: 113 additions & 0 deletions
113
...pKotlin/app/src/test/java/com/android/tv/reference/watchnext/EngageEntityConverterTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
188 changes: 188 additions & 0 deletions
188
...AppKotlin/app/src/test/java/com/android/tv/reference/watchnext/EngageServiceWorkerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
ReferenceAppKotlin/app/src/test/java/com/android/tv/reference/watchnext/PublisherTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |