1 /* 2 * Copyright (C) 2022 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 17 package com.android.safetycenter.testing 18 19 import android.content.Context 20 import android.content.Intent 21 import android.os.Build.VERSION_CODES.TIRAMISU 22 import android.os.UserHandle 23 import android.safetycenter.SafetyCenterManager 24 import android.safetycenter.SafetyCenterManager.ACTION_REFRESH_SAFETY_SOURCES 25 import android.safetycenter.SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED 26 import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA 27 import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_GET_DATA 28 import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID 29 import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE 30 import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS 31 import android.safetycenter.SafetyEvent 32 import android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED 33 import android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED 34 import android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED 35 import android.safetycenter.SafetySourceData 36 import android.safetycenter.SafetySourceErrorDetails 37 import androidx.annotation.RequiresApi 38 import com.android.safetycenter.testing.SafetySourceTestData.Companion.EVENT_SOURCE_STATE_CHANGED 39 import javax.annotation.concurrent.GuardedBy 40 import kotlinx.coroutines.channels.Channel 41 import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED 42 import kotlinx.coroutines.sync.Mutex 43 import kotlinx.coroutines.sync.withLock 44 45 /** 46 * A class that handles [Intent] sent by Safety Center and interacts with the [SafetyCenterManager] 47 * as a response. 48 * 49 * This is meant to emulate how Safety Sources will typically react to Safety Center intents. 50 */ 51 @RequiresApi(TIRAMISU) 52 class SafetySourceIntentHandler { 53 54 private val refreshSafetySourcesChannel = Channel<String>(UNLIMITED) 55 private val safetyCenterEnabledChangedChannel = Channel<Boolean>(UNLIMITED) 56 private val resolveActionChannel = Channel<Unit>(UNLIMITED) 57 private val dismissIssueChannel = Channel<Unit>(UNLIMITED) 58 private val mutex = Mutex() 59 @GuardedBy("mutex") private val requestsToResponses = mutableMapOf<Request, Response>() 60 61 /** Handles the given [Intent] sent to a Safety Source in the given [Context]. */ handlenull62 suspend fun handle(context: Context, intent: Intent) { 63 val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!! 64 val userId = context.userId 65 when (val action = intent.action) { 66 ACTION_REFRESH_SAFETY_SOURCES -> 67 safetyCenterManager.processRefreshSafetySources(intent, userId) 68 ACTION_SAFETY_CENTER_ENABLED_CHANGED -> 69 safetyCenterEnabledChangedChannel.send(safetyCenterManager.isSafetyCenterEnabled) 70 ACTION_RESOLVE_ACTION -> safetyCenterManager.processResolveAction(intent, userId) 71 ACTION_DISMISS_ISSUE -> safetyCenterManager.processDismissIssue(intent, userId) 72 else -> throw IllegalArgumentException("Unexpected intent action: $action") 73 } 74 } 75 76 /** 77 * Sets the [response] to perform on the [SafetyCenterManager] on incoming [Intent]s matching 78 * the given [request]. 79 */ setResponsenull80 suspend fun setResponse(request: Request, response: Response) { 81 mutex.withLock { requestsToResponses.put(request, response) } 82 } 83 84 /** 85 * Suspends until an [ACTION_REFRESH_SAFETY_SOURCES] [Intent] is processed, and returns the 86 * associated [EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID]. 87 */ receiveRefreshSafetySourcesnull88 suspend fun receiveRefreshSafetySources(): String = refreshSafetySourcesChannel.receive() 89 90 /** 91 * Suspends until an [ACTION_SAFETY_CENTER_ENABLED_CHANGED] [Intent] is processed, and returns 92 * whether the Safety Center is now enabled. 93 */ 94 suspend fun receiveSafetyCenterEnabledChanged(): Boolean = 95 safetyCenterEnabledChangedChannel.receive() 96 97 /** Suspends until an [ACTION_RESOLVE_ACTION] [Intent] is processed. */ 98 suspend fun receiveResolveAction() { 99 resolveActionChannel.receive() 100 } 101 102 /** Suspends until an [ACTION_DISMISS_ISSUE] [Intent] is processed. */ receiveDimissIssuenull103 suspend fun receiveDimissIssue() { 104 dismissIssueChannel.receive() 105 } 106 107 /** Cancels any pending update on this [SafetySourceIntentHandler]. */ cancelnull108 fun cancel() { 109 refreshSafetySourcesChannel.cancel() 110 safetyCenterEnabledChangedChannel.cancel() 111 resolveActionChannel.cancel() 112 dismissIssueChannel.cancel() 113 } 114 processRefreshSafetySourcesnull115 private suspend fun SafetyCenterManager.processRefreshSafetySources( 116 intent: Intent, 117 userId: Int 118 ) { 119 val broadcastId = intent.getStringExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID) 120 if (broadcastId.isNullOrEmpty()) { 121 throw IllegalArgumentException("Received refresh intent with no broadcast id specified") 122 } 123 124 val sourceIds = intent.getStringArrayExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS) 125 if (sourceIds.isNullOrEmpty()) { 126 throw IllegalArgumentException("Received refresh intent with no source ids specified") 127 } 128 129 val requestType = intent.getIntExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, -1) 130 131 for (sourceId in sourceIds) { 132 processRequest(toRefreshSafetySourcesRequest(requestType, sourceId, userId)) { 133 createRefreshEvent( 134 if (it is Response.SetData && it.overrideBroadcastId != null) { 135 it.overrideBroadcastId 136 } else { 137 broadcastId 138 } 139 ) 140 } 141 } 142 143 refreshSafetySourcesChannel.send(broadcastId) 144 } 145 toRefreshSafetySourcesRequestnull146 private fun toRefreshSafetySourcesRequest(requestType: Int, sourceId: String, userId: Int) = 147 when (requestType) { 148 EXTRA_REFRESH_REQUEST_TYPE_GET_DATA -> Request.Refresh(sourceId, userId) 149 EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA -> Request.Rescan(sourceId, userId) 150 else -> throw IllegalStateException("Unexpected request type: $requestType") 151 } 152 createRefreshEventnull153 private fun createRefreshEvent(broadcastId: String) = 154 SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED) 155 .setRefreshBroadcastId(broadcastId) 156 .build() 157 158 private suspend fun SafetyCenterManager.processResolveAction(intent: Intent, userId: Int) { 159 val sourceId = intent.getStringExtra(EXTRA_SOURCE_ID) 160 if (sourceId.isNullOrEmpty()) { 161 throw IllegalArgumentException( 162 "Received resolve action intent with no source id specified" 163 ) 164 } 165 166 val sourceIssueId = intent.getStringExtra(EXTRA_SOURCE_ISSUE_ID) 167 if (sourceIssueId.isNullOrEmpty()) { 168 throw IllegalArgumentException( 169 "Received resolve action intent with no source issue id specified" 170 ) 171 } 172 173 val sourceIssueActionId = intent.getStringExtra(EXTRA_SOURCE_ISSUE_ACTION_ID) 174 if (sourceIssueActionId.isNullOrEmpty()) { 175 throw IllegalArgumentException( 176 "Received resolve action intent with no source issue action id specified" 177 ) 178 } 179 180 processRequest(Request.ResolveAction(sourceId, userId)) { 181 if (it is Response.Error) { 182 createResolveActionErrorEvent(sourceIssueId, sourceIssueActionId) 183 } else { 184 createResolveActionSuccessEvent(sourceIssueId, sourceIssueActionId) 185 } 186 } 187 188 resolveActionChannel.send(Unit) 189 } 190 createResolveActionSuccessEventnull191 private fun createResolveActionSuccessEvent( 192 sourceIssueId: String, 193 sourceIssueActionId: String 194 ) = 195 SafetyEvent.Builder(SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) 196 .setSafetySourceIssueId(sourceIssueId) 197 .setSafetySourceIssueActionId(sourceIssueActionId) 198 .build() 199 200 private fun createResolveActionErrorEvent(sourceIssueId: String, sourceIssueActionId: String) = 201 SafetyEvent.Builder(SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED) 202 .setSafetySourceIssueId(sourceIssueId) 203 .setSafetySourceIssueActionId(sourceIssueActionId) 204 .build() 205 206 private suspend fun SafetyCenterManager.processDismissIssue(intent: Intent, userId: Int) { 207 val sourceId = intent.getStringExtra(EXTRA_SOURCE_ID) 208 if (sourceId.isNullOrEmpty()) { 209 throw IllegalArgumentException( 210 "Received dismiss issue intent with no source id specified" 211 ) 212 } 213 214 processRequest(Request.DismissIssue(sourceId, userId)) { EVENT_SOURCE_STATE_CHANGED } 215 216 dismissIssueChannel.send(Unit) 217 } 218 processRequestnull219 private suspend fun SafetyCenterManager.processRequest( 220 request: Request, 221 safetyEventForResponse: (Response) -> SafetyEvent 222 ) { 223 val response = mutex.withLock { requestsToResponses[request] } ?: return 224 val safetyEvent = response.overrideSafetyEvent ?: safetyEventForResponse(response) 225 when (response) { 226 is Response.Error -> 227 reportSafetySourceError(request.sourceId, SafetySourceErrorDetails(safetyEvent)) 228 is Response.ClearData -> setSafetySourceData(request.sourceId, null, safetyEvent) 229 is Response.SetData -> 230 setSafetySourceData(request.sourceId, response.safetySourceData, safetyEvent) 231 } 232 } 233 234 /** An interface that matches an [Intent] request (or a subset of it). */ 235 sealed interface Request { 236 /** The safety source id this request applies to. */ 237 val sourceId: String 238 239 /** The user id this request applies to. */ 240 val userId: Int 241 242 /** Creates a refresh [Request] based on the given [sourceId] and [userId]. */ 243 data class Refresh( 244 override val sourceId: String, 245 override val userId: Int = UserHandle.myUserId() 246 ) : Request 247 248 /** Creates a rescan [Request] based on the given [sourceId] and [userId]. */ 249 data class Rescan( 250 override val sourceId: String, 251 override val userId: Int = UserHandle.myUserId() 252 ) : Request 253 254 /** Creates a resolve action [Request] based on the given [sourceId] and [userId]. */ 255 data class ResolveAction( 256 override val sourceId: String, 257 override val userId: Int = UserHandle.myUserId() 258 ) : Request 259 260 /** Creates an issue dismissal [Request] based on the given [sourceId] and [userId]. */ 261 data class DismissIssue( 262 override val sourceId: String, 263 override val userId: Int = UserHandle.myUserId() 264 ) : Request 265 } 266 267 /** 268 * An interface that specifies the appropriate action to take on the [SafetyCenterManager] as a 269 * response to an incoming [Request]. 270 */ 271 sealed interface Response { 272 273 /** 274 * If non-null, the [SafetyEvent] to use when calling any applicable [SafetyCenterManager] 275 * methods. 276 */ 277 val overrideSafetyEvent: SafetyEvent? 278 get() = null 279 280 /** Creates an error [Response]. */ 281 object Error : Response 282 283 /** Creates a [Response] to clear the data. */ 284 object ClearData : Response 285 286 /** 287 * Creates a [Response] to set the given [SafetySourceData]. 288 * 289 * @param overrideBroadcastId an optional override of the broadcast id to use in the 290 * [SafetyEvent] sent to the [SafetyCenterManager], in case of [Request.Refresh] or 291 * [Request.Rescan]. This is used to simulate a misuse of the [SafetyCenterManager] APIs 292 * @param overrideSafetyEvent like [overrideBroadcastId] but allows the whole [SafetyEvent] 293 * to be override to send different types of [SafetyEvent]. 294 */ 295 data class SetData( 296 val safetySourceData: SafetySourceData, 297 val overrideBroadcastId: String? = null, 298 override val overrideSafetyEvent: SafetyEvent? = null 299 ) : Response 300 } 301 302 companion object { 303 /** An intent action to handle a resolving action. */ 304 const val ACTION_RESOLVE_ACTION = "com.android.safetycenter.testing.action.RESOLVE_ACTION" 305 306 /** An intent action to handle an issue dismissed by Safety Center. */ 307 const val ACTION_DISMISS_ISSUE = "com.android.safetycenter.testing.action.DISMISS_ISSUE" 308 309 /** 310 * An extra to be used with [ACTION_RESOLVE_ACTION] or [ACTION_DISMISS_ISSUE] to specify the 311 * target safety source id. 312 */ 313 const val EXTRA_SOURCE_ID = "com.android.safetycenter.testing.extra.SOURCE_ID" 314 315 /** 316 * An extra to be used with [ACTION_RESOLVE_ACTION] to specify the target safety source 317 * issue id of the resolving action. 318 */ 319 const val EXTRA_SOURCE_ISSUE_ID = "com.android.safetycenter.testing.extra.SOURCE_ISSUE_ID" 320 321 /** 322 * An extra to be used with [ACTION_RESOLVE_ACTION] to specify the target safety source 323 * issue action id of the resolving action. 324 */ 325 const val EXTRA_SOURCE_ISSUE_ACTION_ID = 326 "com.android.safetycenter.testing.extra.SOURCE_ISSUE_ACTION_ID" 327 } 328 } 329