Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9aac2bd authored by William Loh's avatar William Loh Committed by Android (Google) Code Review
Browse files

Merge "Add a periodic update worker for verified domains" into main

parents c61e6d8b 785dec24
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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))
        }
    }
}
+39 −0
Original line number Diff line number Diff line
@@ -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
                        )
                    }
            }
        }
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -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(
+16 −1
Original line number Diff line number Diff line
@@ -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
            }
        }
    }
}
+94 −0
Original line number Diff line number Diff line
/*
 * 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
Loading