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