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
18 
19 import android.os.Build.VERSION_CODES.TIRAMISU
20 import android.os.Bundle
21 import android.safetycenter.SafetyCenterErrorDetails
22 import android.widget.Toast
23 import androidx.annotation.RequiresApi
24 import androidx.lifecycle.ViewModelProvider
25 import androidx.preference.PreferenceFragmentCompat
26 import androidx.preference.PreferenceScreen
27 import androidx.recyclerview.widget.RecyclerView
28 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID
29 import com.android.permissioncontroller.Constants.INVALID_SESSION_ID
30 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.QUICK_SETTINGS_SAFETY_CENTER_FRAGMENT
31 import com.android.permissioncontroller.safetycenter.ui.ParsedSafetyCenterIntent.Companion.toSafetyCenterIntent
32 import com.android.permissioncontroller.safetycenter.ui.model.LiveSafetyCenterViewModelFactory
33 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData
34 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel
35 import com.android.safetycenter.resources.SafetyCenterResourcesApk
36 
37 /** A base fragment that represents a page in Safety Center. */
38 @RequiresApi(TIRAMISU)
39 abstract class SafetyCenterFragment : PreferenceFragmentCompat() {
40 
41     lateinit var safetyCenterViewModel: SafetyCenterViewModel
42     lateinit var sameTaskSourceIds: List<String>
43     lateinit var collapsableIssuesCardHelper: CollapsableIssuesCardHelper
44     var safetyCenterSessionId = INVALID_SESSION_ID
45     private val highlightManager = PreferenceHighlightManager(this)
46 
47     override fun onCreate(savedInstanceState: Bundle?) {
48         super.onCreate(savedInstanceState)
49         highlightManager.restoreState(savedInstanceState)
50     }
51 
52     override fun onCreateAdapter(
53         preferenceScreen: PreferenceScreen
54     ): RecyclerView.Adapter<RecyclerView.ViewHolder> {
55         /* The scroll-to-result functionality for settings search is currently implemented only for
56          * subpages i.e. non expand-and-collapse type entries. Hence, we check that the flag is
57          * enabled before using an adapter that does the highlighting and scrolling. */
58         val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
59             if (SafetyCenterUiFlags.getShowSubpages()) {
60                 highlightManager.createAdapter(preferenceScreen)
61             } else {
62                 super.onCreateAdapter(preferenceScreen)
63             }
64 
65         /* By default, the PreferenceGroupAdapter does setHasStableIds(true). Since each Preference
66          * is internally allocated with an auto-incremented ID, it does not allow us to gracefully
67          * update only changed preferences based on SafetyPreferenceComparisonCallback. In order to
68          * allow the list to track the changes, we need to ignore the Preference IDs. */
69         adapter.setHasStableIds(false)
70         return adapter
71     }
72 
73     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
74         sameTaskSourceIds =
75             SafetyCenterResourcesApk(requireContext())
76                 .getStringByName("config_same_task_safety_source_ids")
77                 .split(",")
78         safetyCenterSessionId = requireArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)
79 
80         safetyCenterViewModel =
81             ViewModelProvider(
82                     requireActivity(),
83                     LiveSafetyCenterViewModelFactory(requireActivity().getApplication())
84                 )
85                 .get(SafetyCenterViewModel::class.java)
86         safetyCenterViewModel.safetyCenterUiLiveData.observe(this) { uiData: SafetyCenterUiData? ->
87             renderSafetyCenterData(uiData)
88         }
89         safetyCenterViewModel.errorLiveData.observe(this) { errorDetails: SafetyCenterErrorDetails?
90             ->
91             displayErrorDetails(errorDetails)
92         }
93 
94         val safetyCenterIntent: ParsedSafetyCenterIntent =
95             requireActivity().intent.toSafetyCenterIntent()
96         val isQsFragment =
97             getArguments()?.getBoolean(QUICK_SETTINGS_SAFETY_CENTER_FRAGMENT, false) ?: false
98         collapsableIssuesCardHelper =
99             CollapsableIssuesCardHelper(safetyCenterViewModel, sameTaskSourceIds)
100         collapsableIssuesCardHelper.apply {
101             setFocusedIssueKey(safetyCenterIntent.safetyCenterIssueKey)
102             // Set quick settings state first and allow restored state to override if necessary
103             setQuickSettingsState(isQsFragment, safetyCenterIntent.shouldExpandIssuesGroup)
104             restoreState(savedInstanceState)
105         }
106 
107         getPreferenceManager().setPreferenceComparisonCallback(SafetyPreferenceComparisonCallback())
108     }
109 
110     override fun onBindPreferences() {
111         super.onBindPreferences()
112         highlightManager.registerObserverIfNeeded()
113     }
114 
115     override fun onUnbindPreferences() {
116         super.onUnbindPreferences()
117         highlightManager.unregisterObserverIfNeeded()
118     }
119 
120     override fun onStart() {
121         super.onStart()
122         configureInteractionLogger()
123         logSafetyCenterViewedEvent()
124     }
125 
126     override fun onResume() {
127         super.onResume()
128         highlightManager.highlightPreferenceIfNeeded()
129     }
130 
131     override fun onSaveInstanceState(outState: Bundle) {
132         super.onSaveInstanceState(outState)
133         collapsableIssuesCardHelper.saveState(outState)
134         highlightManager.saveState(outState)
135     }
136 
137     override fun onStop() {
138         super.onStop()
139         safetyCenterViewModel.interactionLogger.clearViewedIssues()
140     }
141 
142     override fun onDestroy() {
143         super.onDestroy()
144         if (activity?.isChangingConfigurations == true) {
145             safetyCenterViewModel.changingConfigurations()
146         }
147     }
148 
149     /**
150      * Insert preferences for whatever Safety Center data we currently have available.
151      *
152      * This should contain the groups and entries to render the basic page structure, even if no
153      * source has responded with data at this point.
154      *
155      * This should be called by subclasses in [onCreatePreferences] after they've pulled out the
156      * preferences they will modify in [renderSafetyCenterData].
157      */
158     protected fun prerenderCurrentSafetyCenterData() =
159         renderSafetyCenterData(safetyCenterViewModel.getCurrentSafetyCenterDataAsUiData())
160 
161     abstract fun renderSafetyCenterData(uiData: SafetyCenterUiData?)
162 
163     abstract fun configureInteractionLogger()
164 
165     private fun logSafetyCenterViewedEvent() {
166         // If Safety Center was opened due to an associated notification click (i.e. intent has an
167         // associated issue), record that issue's metadata on the SAFETY_CENTER_VIEWED event
168         val maybeIssueKey = requireActivity().intent.toSafetyCenterIntent().safetyCenterIssueKey
169         val maybeIssue =
170             maybeIssueKey?.let {
171                 safetyCenterViewModel.getCurrentSafetyCenterDataAsUiData().getMatchingIssue(it)
172             }
173 
174         if (maybeIssue == null) {
175             safetyCenterViewModel.interactionLogger.record(Action.SAFETY_CENTER_VIEWED)
176         } else {
177             safetyCenterViewModel.interactionLogger.recordForIssue(
178                 Action.SAFETY_CENTER_VIEWED,
179                 maybeIssue,
180                 isDismissed = false
181             )
182         }
183     }
184 
185     private fun displayErrorDetails(errorDetails: SafetyCenterErrorDetails?) {
186         if (errorDetails == null) return
187         Toast.makeText(requireContext(), errorDetails.errorMessage, Toast.LENGTH_LONG).show()
188         safetyCenterViewModel.clearError()
189     }
190 }
191