• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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