-
Notifications
You must be signed in to change notification settings - Fork 344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: use engage-sdk for continue watching #150
base: main
Are you sure you want to change the base?
Changes from all commits
770965e
cb00a54
3aea6eb
1ab940d
bc19b51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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 com.android.tv.reference.shared.datamodel.VideoType | ||
import com.google.android.engage.common.datamodel.ContinuationCluster | ||
import com.google.android.engage.service.PublishContinuationClusterRequest | ||
|
||
/** | ||
* Class in charge of constructing the publishing requests and sending them to their respective | ||
* publishers | ||
*/ | ||
class ClusterRequestFactory(context: Context) { | ||
|
||
private val engageWatchNextService = EngageWatchNextService.getInstance(context) | ||
|
||
/** | ||
* [constructContinuationClusterRequest] returns a [PublishContinuationClusterRequest] to be used | ||
* by the [EngageServiceWorker] to publish Continuations clusters | ||
* | ||
* @return PublishContinuationClusterRequest | ||
*/ | ||
fun constructContinuationClusterRequest(): PublishContinuationClusterRequest { | ||
val continuationList = engageWatchNextService.getAllWatchNextVideos() | ||
val continuationCluster = ContinuationCluster.Builder() | ||
for (watchNextVideo in continuationList) { | ||
val videoType = watchNextVideo.video.videoType | ||
if (videoType == VideoType.EPISODE || videoType == VideoType.MOVIE) { | ||
continuationCluster.addEntity( | ||
VideoToEngageEntityConverter.convertVideo( | ||
watchNextVideo | ||
) | ||
) | ||
} | ||
} | ||
return PublishContinuationClusterRequest.Builder() | ||
.setContinuationCluster(continuationCluster.build()) | ||
.build() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* | ||
* 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 | ||
|
||
/** Constants that are to be used as reference for publishing guidelines */ | ||
object Constants { | ||
const val MAX_PUBLISHING_ATTEMPTS: Int = 5 | ||
|
||
const val WORKER_NAME_CONTINUATION: String = "Upload Continuation" | ||
|
||
const val PERIODIC_WORKER_NAME_CONTINUATION: String = "Periodically Upload Continuation" | ||
|
||
const val PUBLISH_TYPE: String = "PUBLISH_TYPE" | ||
const val PUBLISH_TYPE_CONTINUATION = "PUBLISH_CONTINUATION" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
* 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.BroadcastReceiver | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.util.Log | ||
import com.android.tv.reference.watchnext.Publisher.publishContinuationClusters | ||
import com.google.android.engage.service.Intents.ACTION_PUBLISH_CONTINUATION | ||
|
||
/** Broadcast Receiver to trigger a one time publish of clusters. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggesting to add a link to the document describing why BroadcastReceiver is necessary. |
||
class EngageServiceBroadcastReceiver : BroadcastReceiver() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering this Broadcast Receiver is not registered. Would you please double check it is registered dynamically or statically in the Manifest? |
||
|
||
override fun onReceive(context: Context?, intent: Intent?) { | ||
if (intent == null || context == null) { | ||
return | ||
} | ||
when (intent.action) { | ||
ACTION_PUBLISH_CONTINUATION -> publishContinuationClusters(context) | ||
else -> Log.e(TAG, "onReceive: Received unrecognized intent: $intent") | ||
} | ||
} | ||
|
||
private companion object { | ||
const val TAG = "EngageBroadcastReceiver" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* | ||
* 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 android.util.Log | ||
import androidx.work.Worker | ||
import androidx.work.WorkerParameters | ||
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.AppEngageException | ||
import com.google.android.engage.service.AppEngagePublishClient | ||
import com.google.android.engage.service.AppEngagePublishStatusCode | ||
import com.google.android.engage.service.PublishStatusRequest | ||
import com.google.android.gms.tasks.Task | ||
import com.google.android.gms.tasks.Tasks | ||
import com.google.common.annotations.VisibleForTesting | ||
import timber.log.Timber | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggesting to replace Timber with the standard Log. |
||
|
||
/** | ||
* [EngageServiceWorker] is a [Worker] class that is tasked with publishing cluster | ||
* requests to Engage Service | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you please add a URL of the document describing context of using ServiceWorker to publish cluster requests? |
||
*/ | ||
class EngageServiceWorker( | ||
context: Context, | ||
workerParams: WorkerParameters, | ||
) : Worker(context, workerParams) { | ||
|
||
@VisibleForTesting | ||
constructor( | ||
context: Context, | ||
workerParams: WorkerParameters, | ||
client: AppEngagePublishClient, | ||
) : this(context, workerParams) { | ||
this.client = client | ||
} | ||
|
||
val TAG = "ENGAGE_SERVICE_WORKER" | ||
private var client = AppEngagePublishClient(context) | ||
private val clusterRequestFactory = ClusterRequestFactory(context) | ||
|
||
/** | ||
* [doWork] is the entry point for the [EngageServiceWorker], and differentiates between | ||
* publishing tasks of each cluster | ||
*/ | ||
override fun doWork(): Result { | ||
// If too many publishing attempts have failed, do not attempt to publish again. | ||
if (runAttemptCount > Constants.MAX_PUBLISHING_ATTEMPTS) { | ||
return Result.failure() | ||
} | ||
|
||
Timber.i("Checking for Engage Service availability") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggesting to replace with |
||
|
||
// Check if engage service is available before publishing. | ||
val isAvailable = Tasks.await(client.isServiceAvailable) | ||
|
||
// If the service is not available, do not attempt to publish and indicate failure. | ||
if (!isAvailable) { | ||
Timber.e("Engage service is not available") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggesting to replace with |
||
return Result.failure() | ||
} | ||
|
||
Timber.i("Engage Service is available. Proceeding to publish clusters") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggesting to replace with |
||
|
||
// The cluster to publish must be passed into the worker through the input data. | ||
// This value must be one of the predefined values indicated a valid cluster to publish. Instead | ||
// of using one worker with flags to determine what cluster to publish, you may also choose to | ||
// your separate workers to publish different clusters; use whichever approach better fits your | ||
// app architecture. | ||
return when (inputData.getString(PUBLISH_TYPE)) { | ||
PUBLISH_TYPE_CONTINUATION -> publishContinuation() | ||
else -> throw IllegalArgumentException("Bad publish type") | ||
} | ||
} | ||
|
||
/** | ||
* [publishContinuation] publishes continuation clusters and returns the result of the attempt to | ||
* publish the continuation clusters if the user is signed in. If the user is signed out it | ||
* instead publishes a request to delete the continuation cluster that had previously been | ||
* published. | ||
* | ||
* @return result Result of publishing a continuation cluster, or continuation cluster deletion | ||
*/ | ||
private fun publishContinuation(): Result { | ||
val publishTask: Task<Void> = client.publishContinuationCluster( | ||
clusterRequestFactory.constructContinuationClusterRequest() | ||
) | ||
val statusCode: Int = AppEngagePublishStatusCode.PUBLISHED | ||
return publishAndProvideResult(publishTask, statusCode) | ||
} | ||
|
||
/** | ||
* [publishAndProvideResult] is a method that is in charge of publishing a given task | ||
* | ||
* @param publishTask A task to publish some cluster or delete some cluster | ||
* @param publishStatusCode Publish status code to set through Engage. | ||
* @return publishResult Result of [publishTask] | ||
*/ | ||
private fun publishAndProvideResult( | ||
publishTask: Task<Void>, | ||
publishStatusCode: Int | ||
): Result { | ||
setPublishStatusCode(publishStatusCode) | ||
|
||
// Result initialized to success, it is changed to retry or failure if an exception occurs. | ||
var result: Result = Result.success() | ||
try { | ||
// An AppEngageException may occur while publishing, so we may not be able to await the | ||
// result. | ||
Tasks.await(publishTask) | ||
} catch (publishException: AppEngageException) { | ||
Publisher.logPublishing(publishException) | ||
// Some errors are recoverable, such as a threading issue, some are unrecoverable | ||
// such as a cluster not containing all necessary fields. If an error is recoverable, we | ||
// should attempt to publish again. Setting the result to retry means WorkManager will | ||
// attempt to run the worker again, thus attempting to publish again. | ||
result = | ||
if (Publisher.isErrorRecoverable(publishException)) | ||
Result.retry() | ||
else | ||
Result.failure() | ||
} catch (exception: Exception) { | ||
exception.printStackTrace() | ||
result = Result.failure() | ||
} | ||
// This result is returned back to doWork. | ||
return result | ||
} | ||
|
||
/** | ||
* [setPublishStatusCode] method is in charge of updating the publish status code, which monitors | ||
* the health of the integration with EngageSDK | ||
* | ||
* @param statusCode PublishStatus code to be set through Engage. | ||
*/ | ||
private fun setPublishStatusCode(statusCode: Int) { | ||
client | ||
.updatePublishStatus(PublishStatusRequest.Builder().setStatusCode(statusCode).build()) | ||
.addOnSuccessListener { | ||
Log.i(TAG, "Successfully updated publish status code to $statusCode") | ||
} | ||
.addOnFailureListener { exception -> | ||
Log.e( | ||
TAG, | ||
"Failed to update publish status code to $statusCode\n${exception.stackTrace}" | ||
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggesting to replace Timber with the standard Log