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