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