1 /*
<lambda>null2 * 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.permissioncontroller.safetycenter.ui.model
18
19 import android.app.Application
20 import android.content.Context
21 import android.content.Intent
22 import android.content.Intent.ACTION_SAFETY_CENTER
23 import android.os.Build
24 import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
25 import android.safetycenter.SafetyCenterData
26 import android.safetycenter.SafetyCenterErrorDetails
27 import android.safetycenter.SafetyCenterIssue
28 import android.safetycenter.SafetyCenterManager
29 import android.safetycenter.SafetyCenterStatus
30 import android.util.Log
31 import androidx.annotation.MainThread
32 import androidx.annotation.RequiresApi
33 import androidx.core.content.ContextCompat.getMainExecutor
34 import androidx.lifecycle.LiveData
35 import androidx.lifecycle.MutableLiveData
36 import androidx.lifecycle.ViewModel
37 import androidx.lifecycle.ViewModelProvider
38 import androidx.lifecycle.map
39 import com.android.modules.utils.build.SdkLevel
40 import com.android.permissioncontroller.safetycenter.ui.InteractionLogger
41 import com.android.permissioncontroller.safetycenter.ui.NavigationSource
42 import com.android.safetycenter.internaldata.SafetyCenterIds
43
44 /* A SafetyCenterViewModel that talks to the real backing service for Safety Center. */
45 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
46 class LiveSafetyCenterViewModel(
47 app: Application,
48 private val taskId: Int,
49 private val sameTaskSourceIds: List<String>,
50 ) : SafetyCenterViewModel(app) {
51
52 private val TAG: String = LiveSafetyCenterViewModel::class.java.simpleName
53 override val statusUiLiveData: LiveData<StatusUiData>
54 get() = safetyCenterUiLiveData.map { StatusUiData(it.safetyCenterData) }
55
56 override val safetyCenterUiLiveData: LiveData<SafetyCenterUiData> by this::_safetyCenterLiveData
57 override val errorLiveData: LiveData<SafetyCenterErrorDetails> by this::_errorLiveData
58
59 private val _safetyCenterLiveData = SafetyCenterLiveData()
60 private val _errorLiveData = MutableLiveData<SafetyCenterErrorDetails>()
61
62 override val interactionLogger: InteractionLogger by lazy {
63 // Fetching the config to build this set of source IDs requires IPC, so we do this
64 // initialization lazily.
65 InteractionLogger(safetyCenterManager.safetyCenterConfig)
66 }
67
68 private var changingConfigurations = false
69
70 private val safetyCenterManager = app.getSystemService(SafetyCenterManager::class.java)!!
71
72 override fun getCurrentSafetyCenterDataAsUiData(): SafetyCenterUiData =
73 uiData(safetyCenterManager.safetyCenterData)
74
75 override fun dismissIssue(issue: SafetyCenterIssue) {
76 safetyCenterManager.dismissSafetyCenterIssue(issue.id)
77 }
78
79 override fun executeIssueAction(
80 issue: SafetyCenterIssue,
81 action: SafetyCenterIssue.Action,
82 launchTaskId: Int?,
83 ) {
84 val issueId =
85 if (launchTaskId != null) {
86 SafetyCenterIds.encodeToString(
87 SafetyCenterIds.issueIdFromString(issue.id)
88 .toBuilder()
89 .setTaskId(launchTaskId)
90 .build()
91 )
92 } else {
93 issue.id
94 }
95 safetyCenterManager.executeSafetyCenterIssueAction(issueId, action.id)
96 }
97
98 override fun markIssueResolvedUiCompleted(issueId: IssueId) {
99 _safetyCenterLiveData.markIssueResolvedUiCompleted(issueId)
100 }
101
102 override fun rescan() {
103 safetyCenterManager.refreshSafetySources(
104 SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK
105 )
106 }
107
108 override fun clearError() {
109 _errorLiveData.value = null
110 }
111
112 override fun navigateToSafetyCenter(context: Context, navigationSource: NavigationSource?) {
113 val intent = Intent(ACTION_SAFETY_CENTER)
114
115 navigationSource?.addToIntent(intent)
116
117 context.startActivity(intent)
118 }
119
120 override fun pageOpen() {
121 executeIfNotChangingConfigurations {
122 safetyCenterManager.refreshSafetySources(SafetyCenterManager.REFRESH_REASON_PAGE_OPEN)
123 }
124 }
125
126 @RequiresApi(UPSIDE_DOWN_CAKE)
127 override fun pageOpen(sourceGroupId: String) {
128 executeIfNotChangingConfigurations {
129 val safetySourceIds = getSafetySourceIdsToRefresh(sourceGroupId)
130 if (safetySourceIds == null) {
131 Log.w(TAG, "$sourceGroupId has no matching source IDs, so refreshing all sources")
132 safetyCenterManager.refreshSafetySources(
133 SafetyCenterManager.REFRESH_REASON_PAGE_OPEN
134 )
135 } else {
136 safetyCenterManager.refreshSafetySources(
137 SafetyCenterManager.REFRESH_REASON_PAGE_OPEN,
138 safetySourceIds,
139 )
140 }
141 }
142 }
143
144 override fun changingConfigurations() {
145 changingConfigurations = true
146 }
147
148 private fun executeIfNotChangingConfigurations(block: () -> Unit) {
149 if (changingConfigurations) {
150 // Don't refresh when changing configurations, but reset for the next pageOpen call
151 changingConfigurations = false
152 return
153 }
154
155 block()
156 }
157
158 private fun getSafetySourceIdsToRefresh(sourceGroupId: String): List<String>? {
159 val safetySourcesGroup =
160 safetyCenterManager.safetyCenterConfig?.safetySourcesGroups?.find {
161 it.id == sourceGroupId
162 }
163 return safetySourcesGroup?.safetySources?.map { it.id }
164 }
165
166 private inner class SafetyCenterLiveData :
167 MutableLiveData<SafetyCenterUiData>(),
168 SafetyCenterManager.OnSafetyCenterDataChangedListener {
169
170 // Managing the data queue isn't designed to support multithreading. Any methods that
171 // manipulate it, or the inFlight or resolved issues lists should only be called on the
172 // main thread, and are marked accordingly.
173 private val safetyCenterDataQueue = ArrayDeque<SafetyCenterData>()
174 private var issuesPendingResolution = mapOf<IssueId, ActionId>()
175 private val currentResolvedIssues = mutableMapOf<IssueId, ActionId>()
176
177 override fun onActive() {
178 safetyCenterManager.addOnSafetyCenterDataChangedListener(
179 getMainExecutor(app.applicationContext),
180 this,
181 )
182 super.onActive()
183 }
184
185 override fun onInactive() {
186 safetyCenterManager.removeOnSafetyCenterDataChangedListener(this)
187
188 if (!changingConfigurations) {
189 // Remove all the tracked state and start from scratch when active again.
190 issuesPendingResolution = mapOf()
191 currentResolvedIssues.clear()
192 safetyCenterDataQueue.clear()
193 }
194 super.onInactive()
195 }
196
197 @MainThread
198 override fun onSafetyCenterDataChanged(data: SafetyCenterData) {
199 safetyCenterDataQueue.addLast(data)
200 maybeProcessDataToNextResolvedIssues()
201 }
202
203 override fun onError(errorDetails: SafetyCenterErrorDetails) {
204 _errorLiveData.value = errorDetails
205 }
206
207 @MainThread
208 private fun maybeProcessDataToNextResolvedIssues() {
209 // Only process data updates while we aren't waiting for issue resolution animations
210 // to complete.
211 if (currentResolvedIssues.isNotEmpty()) {
212 Log.d(
213 TAG,
214 "Received SafetyCenterData while issue resolution animations" +
215 " occurring. Will update UI with new data soon.",
216 )
217 return
218 }
219
220 while (safetyCenterDataQueue.isNotEmpty() && currentResolvedIssues.isEmpty()) {
221 val nextData = safetyCenterDataQueue.first()
222
223 // Calculate newly resolved issues by diffing the tracked in-flight issues and the
224 // current update. Resolved issues are formerly in-flight issues that no longer
225 // appear in a subsequent SafetyCenterData update.
226 val nextResolvedIssues: Map<IssueId, ActionId> =
227 determineResolvedIssues(nextData.buildIssueIdSet())
228
229 // Save the set of in-flight issues to diff against the next data update, removing
230 // the now-resolved, formerly in-flight issues. If these are not tracked separately
231 // the queue will not progress once the issue resolution animations complete.
232 issuesPendingResolution = nextData.getInFlightIssues()
233
234 if (nextResolvedIssues.isNotEmpty()) {
235 currentResolvedIssues.putAll(nextResolvedIssues)
236 sendResolvedIssuesAndCurrentData()
237 } else if (shouldEndScan(nextData) || shouldSendLastDataInQueue()) {
238 sendNextData()
239 } else {
240 skipNextData()
241 }
242 }
243 }
244
245 private fun determineResolvedIssues(nextIssueIds: Set<IssueId>): Map<IssueId, ActionId> {
246 // Any previously in-flight issue that does not appear in the incoming SafetyCenterData
247 // is considered resolved.
248 return issuesPendingResolution.filterNot { issue -> nextIssueIds.contains(issue.key) }
249 }
250
251 private fun shouldEndScan(nextData: SafetyCenterData): Boolean =
252 isCurrentlyScanning() && !nextData.isScanning()
253
254 private fun shouldSendLastDataInQueue(): Boolean =
255 !isCurrentlyScanning() && safetyCenterDataQueue.size == 1
256
257 private fun isCurrentlyScanning(): Boolean = value?.safetyCenterData?.isScanning() ?: false
258
259 private fun sendNextData() {
260 value = uiData(safetyCenterDataQueue.removeFirst())
261 }
262
263 private fun skipNextData() = safetyCenterDataQueue.removeFirst()
264
265 private fun sendResolvedIssuesAndCurrentData() {
266 val currentData = value?.safetyCenterData
267 if (currentData == null || currentResolvedIssues.isEmpty()) {
268 // There can only be resolved issues after receiving data with in-flight issues,
269 // so we should always have already sent data here.
270 throw IllegalArgumentException("No current data or no resolved issues")
271 }
272
273 // The current SafetyCenterData still contains the resolved SafetyCenterIssue objects.
274 // Send it with the resolved IDs so the UI can generate the correct preferences and
275 // trigger the right animations for issue resolution.
276 value = uiData(currentData, currentResolvedIssues)
277 }
278
279 @MainThread
280 fun markIssueResolvedUiCompleted(issueId: IssueId) {
281 currentResolvedIssues.remove(issueId)
282 maybeProcessDataToNextResolvedIssues()
283 }
284 }
285
286 private fun uiData(
287 safetyCenterData: SafetyCenterData,
288 resolvedIssues: Map<IssueId, ActionId> = emptyMap(),
289 ) = SafetyCenterUiData(safetyCenterData, taskId, sameTaskSourceIds, resolvedIssues)
290 }
291
292 /** Returns inflight issues pending resolution */
SafetyCenterDatanull293 private fun SafetyCenterData.getInFlightIssues(): Map<IssueId, ActionId> =
294 allResolvableIssues
295 .map { issue ->
296 issue.actions
297 // UX requirements require skipping resolution UI for issues that do not have a
298 // valid successMessage
299 .filter { it.isInFlight && !it.successMessage.isNullOrEmpty() }
300 .map { issue.id to it.id }
301 }
302 .flatten()
303 .toMap()
304
SafetyCenterDatanull305 private fun SafetyCenterData.isScanning() =
306 status.refreshStatus == SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS
307
308 private fun SafetyCenterData.buildIssueIdSet(): Set<IssueId> =
309 allResolvableIssues.map { it.id }.toSet()
310
311 private val SafetyCenterData.allResolvableIssues: Sequence<SafetyCenterIssue>
312 get() =
313 if (SdkLevel.isAtLeastU()) {
314 issues.asSequence() + dismissedIssues.asSequence()
315 } else {
316 issues.asSequence()
317 }
318
319 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
320 class LiveSafetyCenterViewModelFactory
321 @JvmOverloads
322 constructor(
323 private val app: Application,
324 private val taskId: Int = 0,
325 private val sameTaskSourceIds: List<String> = emptyList(),
326 ) : ViewModelProvider.Factory {
createnull327 override fun <T : ViewModel> create(modelClass: Class<T>): T {
328 @Suppress("UNCHECKED_CAST")
329 return LiveSafetyCenterViewModel(app, taskId, sameTaskSourceIds) as T
330 }
331 }
332