diff options
author | 2024-11-22 15:26:59 -0800 | |
---|---|---|
committer | 2024-11-22 17:33:46 -0800 | |
commit | 785dec24741e01ba8df15945514fd3e581593bf8 (patch) | |
tree | 1acb126fef4580372d731bafe550a40b4bb3bf75 | |
parent | 2883526fde1414236d8fa49974de014f05d344fc (diff) |
Add a periodic update worker for verified domains
This verified domain update worker is only meant to be a reference
implementation is hardcoded to be disabled. Instead of enabling this
worker, it is highly recommended to implement a custom worker that pulls
updates form a caching service and not from websites directly.
Bug: 307557449
Test: presubmit
Flag: EXEMPT external library
Change-Id: If7dbb1617e4add67c581ef0cc301f8ad978b461e
7 files changed, 185 insertions, 72 deletions
diff --git a/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt index 021a5143c09a..6af8004c4015 100644 --- a/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt +++ b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt @@ -30,6 +30,7 @@ class StatementServiceApplication : Application() { // WorkManager can only schedule when the user data directories are unencrypted (after // the user has entered their lock password. DomainVerificationUtils.schedulePeriodicCheckUnlocked(WorkManager.getInstance(this)) + DomainVerificationUtils.schedulePeriodicUpdateUnlocked(WorkManager.getInstance(this)) } } } diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt index 694424822b30..157a800d0e09 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt @@ -22,6 +22,7 @@ import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import com.android.statementservice.domain.worker.RetryRequestWorker +import com.android.statementservice.domain.worker.UpdateVerifiedDomainsWorker import java.time.Duration object DomainVerificationUtils { @@ -30,6 +31,10 @@ object DomainVerificationUtils { private const val PERIODIC_SHORT_HOURS = 24L private const val PERIODIC_LONG_ID = "retry_long" private const val PERIODIC_LONG_HOURS = 72L + private const val PERIODIC_UPDATE_ID = "update" + private const val PERIODIC_UPDATE_HOURS = 720L + + private const val UPDATE_WORKER_ENABLED = false /** * In a majority of cases, the initial requests will be enough to verify domains, since they @@ -74,4 +79,38 @@ object DomainVerificationUtils { } } } + + /** + * Schedule a periodic worker to check for any updates to assetlink.json files for domains that + * have already been verified. + * + * Due to the potential for this worker to generate enough traffic across all android devices + * to overwhelm websites, this method is hardcoded to be disabled by default. It is highly + * recommended to not enable this worker and instead implement a custom worker that pulls + * updates from a caching service instead of directly from websites. + */ + fun schedulePeriodicUpdateUnlocked(workManager: WorkManager) { + if (UPDATE_WORKER_ENABLED) { + workManager.apply { + PeriodicWorkRequestBuilder<UpdateVerifiedDomainsWorker>( + Duration.ofDays( + PERIODIC_UPDATE_HOURS + ) + ) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresDeviceIdle(true) + .build() + ) + .build() + .let { + enqueueUniquePeriodicWork( + PERIODIC_UPDATE_ID, + ExistingPeriodicWorkPolicy.KEEP, it + ) + } + } + } + } } diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt index 6914347544de..c7f6c184fd22 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt @@ -64,7 +64,8 @@ class DomainVerifier private constructor( private val targetAssetCache = AssetLruCache() - fun collectHosts(packageNames: Iterable<String>): Iterable<Triple<UUID, String, String>> { + fun collectHosts(packageNames: Iterable<String>, statusFilter: (Int) -> Boolean): + Iterable<Triple<UUID, String, Iterable<String>>> { return packageNames.mapNotNull { packageName -> val (domainSetId, _, hostToStateMap) = try { manager.getDomainVerificationInfo(packageName) @@ -74,14 +75,13 @@ class DomainVerifier private constructor( } ?: return@mapNotNull null val hostsToRetry = hostToStateMap - .filterValues(VerifyStatus::shouldRetry) + .filterValues(statusFilter) .takeIf { it.isNotEmpty() } ?.map { it.key } ?: return@mapNotNull null - hostsToRetry.map { Triple(domainSetId, packageName, it) } + Triple(domainSetId, packageName, hostsToRetry) } - .flatten() } suspend fun verifyHost( diff --git a/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt index 2193ec542238..c771da3ab563 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt @@ -49,7 +49,7 @@ enum class VerifyStatus(val value: Int) { return false } - val status = values().find { it.value == state } ?: return true + val status = entries.find { it.value == state } ?: return true return when (status) { SUCCESS, FAILURE_LEGACY_UNSUPPORTED_WILDCARD, @@ -62,5 +62,20 @@ enum class VerifyStatus(val value: Int) { FAILURE_REDIRECT -> true } } + + fun canUpdate(state: Int): Boolean { + if (state == DomainVerificationInfo.STATE_UNMODIFIABLE) { + return false + } + + val status = entries.find { it.value == state } + return when (status) { + SUCCESS, + FAILURE_LEGACY_UNSUPPORTED_WILDCARD, + FAILURE_REJECTED_BY_SERVER, + UNKNOWN -> true + else -> false + } + } } } diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/PeriodicUpdateWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/PeriodicUpdateWorker.kt new file mode 100644 index 000000000000..7ec6e6c28e9b --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/PeriodicUpdateWorker.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import android.content.UriRelativeFilterGroup +import android.content.pm.verify.domain.DomainVerificationManager +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import com.android.statementservice.domain.VerifyStatus +import com.android.statementservice.utils.AndroidUtils +import com.android.statementservice.utils.StatementUtils +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive + +abstract class PeriodicUpdateWorker( + appContext: Context, + params: WorkerParameters +) : BaseRequestWorker(appContext, params) { + + data class VerifyResult( + val host: String, + val status: VerifyStatus, + val groups: List<UriRelativeFilterGroup> + ) + + protected suspend fun updateDomainVerificationStatus(verifyStatusFilter: (Int) -> Boolean): + ListenableWorker.Result { + return coroutineScope { + if (!AndroidUtils.isReceiverV2Enabled(appContext)) { + return@coroutineScope Result.success() + } + + val packageNames = verificationManager.queryValidVerificationPackageNames() + + verifier.collectHosts(packageNames, verifyStatusFilter) + .map { (domainSetId, packageName, hosts) -> + hosts.map { host -> + async { + if (isActive && !isStopped) { + val (_, status, statement) = verifier.verifyHost( + host, + packageName, + params.network + ) + val groups = statement?.dynamicAppLinkComponents.orEmpty().map { + StatementUtils.createUriRelativeFilterGroup(it) + } + VerifyResult(host, status, groups) + } else { + // If the job gets cancelled, stop the remaining hosts, but continue the + // job to commit the results for hosts that were already requested. + null + } + } + }.awaitAll().filterNotNull().groupBy { it.status } + .forEach { (status, results) -> + val error = verificationManager.setDomainVerificationStatus( + domainSetId, + results.map { it.host }.toSet(), + status.value + ) + if (error == DomainVerificationManager.STATUS_OK + && status == VerifyStatus.SUCCESS + ) { + updateUriRelativeFilterGroups( + packageName, + results.associateBy({ it.host }, { it.groups }) + ) + } + } + } + + // Succeed regardless of results since this retry is best effort and not required + Result.success() + } + } +}
\ No newline at end of file diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt index f83601a7807b..e8b4df9b0943 100644 --- a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt @@ -17,18 +17,9 @@ package com.android.statementservice.domain.worker import android.content.Context -import android.content.UriRelativeFilterGroup -import android.content.pm.verify.domain.DomainVerificationManager import androidx.work.NetworkType import androidx.work.WorkerParameters import com.android.statementservice.domain.VerifyStatus -import com.android.statementservice.utils.AndroidUtils -import com.android.statementservice.utils.StatementUtils -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive -import java.util.UUID /** * Scheduled every 24 hours with [NetworkType.CONNECTED] and every 72 hours without any constraints @@ -37,63 +28,7 @@ import java.util.UUID class RetryRequestWorker( appContext: Context, params: WorkerParameters -) : BaseRequestWorker(appContext, params) { +) : PeriodicUpdateWorker(appContext, params) { - data class VerifyResult( - val domainSetId: UUID, - val host: String, - val status: VerifyStatus, - val packageName: String, - val groups: List<UriRelativeFilterGroup> - ) - - override suspend fun doWork() = coroutineScope { - if (!AndroidUtils.isReceiverV2Enabled(appContext)) { - return@coroutineScope Result.success() - } - - val packageNames = verificationManager.queryValidVerificationPackageNames() - - verifier.collectHosts(packageNames) - .map { (domainSetId, packageName, host) -> - async { - if (isActive && !isStopped) { - val (_, status, statement) = verifier.verifyHost(host, packageName, params.network) - val groups = statement?.dynamicAppLinkComponents.orEmpty().map { - StatementUtils.createUriRelativeFilterGroup(it) - } - VerifyResult(domainSetId, host, status, packageName, groups) - } else { - // If the job gets cancelled, stop the remaining hosts, but continue the - // job to commit the results for hosts that were already requested. - null - } - } - } - .awaitAll() - .filterNotNull() // TODO(b/159952358): Fast fail packages which can't be retrieved. - .groupBy { it.packageName } - .forEach { (packageName, resultsByName) -> - val groupUpdates = mutableMapOf<String, List<UriRelativeFilterGroup>>() - resultsByName.groupBy { it.domainSetId } - .forEach { (domainSetId, resultsById) -> - resultsById.groupBy { it.status } - .forEach { (status, verifyResults) -> - val error = verificationManager.setDomainVerificationStatus( - domainSetId, - verifyResults.map(VerifyResult::host).toSet(), - status.value - ) - if (error == DomainVerificationManager.STATUS_OK - && status == VerifyStatus.SUCCESS) { - verifyResults.forEach { groupUpdates[it.host] = it.groups } - } - } - } - updateUriRelativeFilterGroups(packageName, groupUpdates) - } - - // Succeed regardless of results since this retry is best effort and not required - Result.success() - } + override suspend fun doWork() = updateDomainVerificationStatus(VerifyStatus::shouldRetry) } diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/UpdateVerifiedDomainsWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/UpdateVerifiedDomainsWorker.kt new file mode 100644 index 000000000000..c6f40c832860 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/domain/worker/UpdateVerifiedDomainsWorker.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.statementservice.domain.worker + +import android.content.Context +import androidx.work.WorkerParameters +import com.android.statementservice.domain.VerifyStatus + +class UpdateVerifiedDomainsWorker( + appContext: Context, + params: WorkerParameters +) : PeriodicUpdateWorker(appContext, params) { + + override suspend fun doWork() = updateDomainVerificationStatus(VerifyStatus::canUpdate) +}
\ No newline at end of file |