1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.safetycenter.testing 17 18 import android.util.Log 19 import androidx.annotation.GuardedBy 20 import com.android.compatibility.common.util.SystemUtil 21 import com.android.safetycenter.testing.Coroutines.TIMEOUT_LONG 22 import com.android.safetycenter.testing.Coroutines.runBlockingWithTimeout 23 import kotlinx.coroutines.DelicateCoroutinesApi 24 import kotlinx.coroutines.Dispatchers 25 import kotlinx.coroutines.GlobalScope 26 import kotlinx.coroutines.Job 27 import kotlinx.coroutines.TimeoutCancellationException 28 import kotlinx.coroutines.launch 29 import kotlinx.coroutines.sync.Mutex 30 import kotlinx.coroutines.sync.withLock 31 32 /** A class to help waiting on broadcasts to be processed by the system. */ 33 object WaitForBroadcasts { 34 35 private const val TAG: String = "WaitForBroadcasts" 36 37 private val mutex = Mutex() 38 @GuardedBy("mutex") private var currentJob: Job? = null 39 40 /** 41 * Waits for broadcasts for at most [TIMEOUT_LONG] and prints a warning if that operation timed 42 * out. 43 * 44 * The [SystemUtil.waitForBroadcasts] operation will keep on running even after the timeout as 45 * it is not interruptible. Further calls to [WaitForBroadcasts.waitForBroadcasts] will re-use 46 * the currently running [SystemUtil.waitForBroadcasts] call if it hasn't completed. 47 */ waitForBroadcastsnull48 fun waitForBroadcasts() { 49 try { 50 runBlockingWithTimeout { 51 mutex 52 .withLock { 53 val newJob = currentJob.maybeStartNewWaitForBroadcasts() 54 currentJob = newJob 55 newJob 56 } 57 .join() 58 } 59 } catch (e: TimeoutCancellationException) { 60 Log.w(TAG, "Waiting for broadcasts timed out, proceeding anyway", e) 61 } 62 } 63 64 // We're using a GlobalScope here as there doesn't seem to be a straightforward way to timeout 65 // and interrupt the waitForBroadcasts() call. Given it's uninterruptible, we'd rather just have 66 // at most one globally-bound waitForBroadcasts() call running at any given time. 67 @OptIn(DelicateCoroutinesApi::class) maybeStartNewWaitForBroadcastsnull68 private fun Job?.maybeStartNewWaitForBroadcasts(): Job = 69 if (this != null && isActive) { 70 this 71 } else { <lambda>null72 GlobalScope.launch(Dispatchers.IO) { SystemUtil.waitForBroadcasts() } 73 } 74 } 75