<lambda>null1 @file:Suppress("DEPRECATION")
2
3 package com.android.permissioncontroller.permission.ui.handheld.v34
4
5 import android.app.Application
6 import android.graphics.Color
7 import android.graphics.drawable.ColorDrawable
8 import android.graphics.drawable.Drawable
9 import android.os.Build
10 import android.os.Bundle
11 import android.os.UserHandle
12 import android.util.Log
13 import android.view.MenuItem
14 import android.view.View
15 import androidx.annotation.RequiresApi
16 import androidx.preference.Preference
17 import androidx.preference.PreferenceCategory
18 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID
19 import com.android.permissioncontroller.Constants.INVALID_SESSION_ID
20 import com.android.permissioncontroller.PermissionControllerStatsLog
21 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED
22 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_ADVERTISING_PURPOSE
23 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_SHARING_WITHOUT_ADVERTISING_PURPOSE
24 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_SHARING_WITH_ADVERTISING_PURPOSE
25 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED
26 import com.android.permissioncontroller.R
27 import com.android.permissioncontroller.permission.model.v34.DataSharingUpdateType
28 import com.android.permissioncontroller.permission.ui.handheld.PermissionsFrameFragment
29 import com.android.permissioncontroller.permission.ui.handheld.pressBack
30 import com.android.permissioncontroller.permission.ui.model.v34.AppDataSharingUpdatesViewModel
31 import com.android.permissioncontroller.permission.ui.model.v34.AppDataSharingUpdatesViewModel.AppLocationDataSharingUpdateUiInfo
32 import com.android.permissioncontroller.permission.utils.KotlinUtils
33 import com.android.permissioncontroller.permission.utils.StringUtils
34 import java.text.Collator
35 import java.util.Random
36
37 /** Fragment to display data sharing updates for installed apps. */
38 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
39 class AppDataSharingUpdatesFragment : PermissionsFrameFragment() {
40 private lateinit var viewModel: AppDataSharingUpdatesViewModel
41 private lateinit var collator: Collator
42 private var sessionId: Long = INVALID_SESSION_ID
43
44 override fun onCreate(savedInstanceState: Bundle?) {
45 super.onCreate(savedInstanceState)
46 requireActivity().title = getString(R.string.data_sharing_updates_title)
47 setHasOptionsMenu(true)
48 collator = Collator.getInstance(requireContext().resources.configuration.locales[0])
49
50 val ab = activity?.actionBar
51 ab?.setDisplayHomeAsUpEnabled(true)
52
53 viewModel = AppDataSharingUpdatesViewModel(requireActivity().application)
54
55 if (preferenceScreen == null) {
56 addPreferencesFromResource(R.xml.app_data_sharing_updates)
57 setLoading(/* loading= */ true, /* animate= */ false)
58 }
59
60 viewModel.appLocationDataSharingUpdateUiInfoLiveData.observe(this, this::updatePreferences)
61 sessionId =
62 activity?.intent?.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID)
63 ?: INVALID_SESSION_ID
64 while (sessionId == INVALID_SESSION_ID) {
65 sessionId = Random().nextLong()
66 }
67 }
68
69 override fun setDivider(divider: Drawable?) {
70 super.setDivider(ColorDrawable(Color.TRANSPARENT))
71 }
72
73 override fun onOptionsItemSelected(item: MenuItem): Boolean {
74 if (item.itemId == android.R.id.home) {
75 this.pressBack()
76 return true
77 }
78
79 return super.onOptionsItemSelected(item)
80 }
81
82 private fun updatePreferences(updateUiInfos: List<AppLocationDataSharingUpdateUiInfo>) {
83 setLoading(/* loading= */ false, /* animate= */ true)
84
85 logAppDataSharingUpdatesFragmentViewed(sessionId, updateUiInfos.size)
86
87 if (updateUiInfos.isNotEmpty()) {
88 showUpdatesPresentUi()
89 } else {
90 showNoUpdatesPresentUi()
91 }
92
93 val preferenceKeysToShow =
94 updateUiInfos.map {
95 createUpdatePreferenceKey(it.packageName, it.userHandle, it.dataSharingUpdateType)
96 }
97
98 val updatesCategory =
99 preferenceScreen.findPreference<PreferenceCategory>(
100 LAST_PERIOD_UPDATES_PREFERENCE_CATEGORY_ID
101 )
102 ?: return
103
104 val preferencesToRemove = mutableSetOf<Preference>()
105 for (i in 0 until (updatesCategory.preferenceCount)) {
106 if (!preferenceKeysToShow.contains(updatesCategory.getPreference(i).key)) {
107 preferencesToRemove.add(updatesCategory.getPreference(i))
108 }
109 }
110 // Remove preferences that no longer need to be shown.
111 preferencesToRemove.forEach { updatesCategory.removePreference(it) }
112
113 updateUiInfos.forEach { updateUiInfo ->
114 val key =
115 createUpdatePreferenceKey(
116 updateUiInfo.packageName,
117 updateUiInfo.userHandle,
118 updateUiInfo.dataSharingUpdateType
119 )
120 if (updatesCategory.findPreference<AppDataSharingUpdatePreference>(key) != null) {
121 // If a preference is already shown, don't recreate it.
122 return@forEach
123 }
124 val appDataSharingUpdatePreference =
125 AppDataSharingUpdatePreference(
126 requireActivity().application,
127 updateUiInfo.packageName,
128 updateUiInfo.userHandle,
129 requireActivity().applicationContext
130 )
131 appDataSharingUpdatePreference.apply {
132 this.key = key
133 title =
134 KotlinUtils.getPackageLabel(
135 requireActivity().application,
136 updateUiInfo.packageName,
137 updateUiInfo.userHandle
138 )
139 summary = getSummaryForLocationUpdateType(updateUiInfo.dataSharingUpdateType)
140 preferenceClick =
141 View.OnClickListener { _ ->
142 logAppDataSharingUpdatesFragmentActionReported(
143 sessionId,
144 requireActivity().application,
145 updateUiInfo
146 )
147 viewModel.startAppLocationPermissionPage(
148 requireActivity(),
149 sessionId,
150 updateUiInfo.packageName,
151 updateUiInfo.userHandle
152 )
153 }
154 updatesCategory.addPreference(this)
155 }
156 }
157 KotlinUtils.sortPreferenceGroup(updatesCategory, this::compareUpdatePreferences, false)
158 }
159
160 private fun getSummaryForLocationUpdateType(type: DataSharingUpdateType): String {
161 return when (type) {
162 DataSharingUpdateType.ADDS_ADVERTISING_PURPOSE ->
163 getString(R.string.shares_location_with_third_parties_for_advertising)
164 DataSharingUpdateType.ADDS_SHARING_WITHOUT_ADVERTISING_PURPOSE ->
165 getString(R.string.shares_location_with_third_parties)
166 DataSharingUpdateType.ADDS_SHARING_WITH_ADVERTISING_PURPOSE ->
167 getString(R.string.shares_location_with_third_parties_for_advertising)
168 }
169 }
170
171 private fun showUpdatesPresentUi() {
172 if (preferenceScreen == null) {
173 return
174 }
175 val detailsPreference =
176 preferenceScreen?.findPreference<AppDataSharingDetailsPreference>(DETAILS_PREFERENCE_ID)
177 val footerPreference =
178 preferenceScreen?.findPreference<AppDataSharingUpdatesFooterPreference>(
179 FOOTER_PREFERENCE_ID
180 )
181 val dataSharingUpdatesCategory =
182 preferenceScreen?.findPreference<PreferenceCategory>(
183 LAST_PERIOD_UPDATES_PREFERENCE_CATEGORY_ID
184 )
185
186 detailsPreference?.let {
187 it.showNoUpdates = false
188 it.isVisible = true
189 }
190 dataSharingUpdatesCategory?.let {
191 it.title =
192 StringUtils.getIcuPluralsString(requireContext(), R.string.updated_in_last_days, 30)
193 it.isVisible = true
194 }
195
196 val onFooterLinkClick =
197 if (viewModel.canLinkToHelpCenter(requireActivity())) {
198 View.OnClickListener { viewModel.openSafetyLabelsHelpCenterPage(requireActivity()) }
199 } else {
200 null
201 }
202 footerPreference?.let {
203 it.footerMessage = getString(R.string.data_sharing_updates_footer_message)
204 it.footerLink = getString(R.string.learn_about_data_sharing)
205 it.onFooterLinkClick = onFooterLinkClick
206 it.isVisible = true
207 }
208 }
209
210 // TODO(b/261666772): Once spec is final, consider extracting common elements with
211 // showUpdatesPresentUi into a separate method.
212 private fun showNoUpdatesPresentUi() {
213 if (preferenceScreen == null) {
214 return
215 }
216 val detailsPreference =
217 preferenceScreen?.findPreference<AppDataSharingDetailsPreference>(DETAILS_PREFERENCE_ID)
218 val footerPreference =
219 preferenceScreen?.findPreference<AppDataSharingUpdatesFooterPreference>(
220 FOOTER_PREFERENCE_ID
221 )
222 val dataSharingUpdatesCategory =
223 preferenceScreen?.findPreference<PreferenceCategory>(
224 LAST_PERIOD_UPDATES_PREFERENCE_CATEGORY_ID
225 )
226
227 detailsPreference?.let {
228 it.showNoUpdates = true
229 it.isVisible = true
230 }
231 dataSharingUpdatesCategory?.let { it.isVisible = false }
232
233 footerPreference?.let {
234 it.footerMessage = getString(R.string.data_sharing_updates_footer_message)
235 it.footerLink = getString(R.string.learn_about_data_sharing)
236 it.onFooterLinkClick =
237 if (viewModel.canLinkToHelpCenter(requireActivity())) {
238 View.OnClickListener {
239 viewModel.openSafetyLabelsHelpCenterPage(requireActivity())
240 }
241 } else {
242 null
243 }
244 it.isVisible = true
245 }
246 }
247
248 /** Creates an identifier for a preference. */
249 private fun createUpdatePreferenceKey(
250 packageName: String,
251 user: UserHandle,
252 update: DataSharingUpdateType
253 ): String {
254 return "$packageName:${user.identifier}:${update.name}"
255 }
256
257 private fun compareUpdatePreferences(lhs: Preference, rhs: Preference): Int {
258 var result = collator.compare(lhs.title.toString(), rhs.title.toString())
259 if (result == 0) {
260 result = lhs.key.compareTo(rhs.key)
261 }
262 return result
263 }
264
265 /** Companion object for [AppDataSharingUpdatesFragment]. */
266 companion object {
267 /**
268 * Creates a [Bundle] with the arguments needed by this fragment.
269 *
270 * @param sessionId the current session ID
271 * @return a [Bundle] with all of the required args
272 */
273 fun createArgs(sessionId: Long) = Bundle().apply { putLong(EXTRA_SESSION_ID, sessionId) }
274
275 private val LOG_TAG = AppDataSharingUpdatesFragment::class.java.simpleName
276
277 private const val DETAILS_PREFERENCE_ID = "details"
278 private const val FOOTER_PREFERENCE_ID = "info_footer"
279 private const val LAST_PERIOD_UPDATES_PREFERENCE_CATEGORY_ID = "last_period_updates"
280
281 private fun logAppDataSharingUpdatesFragmentViewed(
282 sessionId: Long,
283 numberOfAppUpdates: Int
284 ) {
285 PermissionControllerStatsLog.write(
286 APP_DATA_SHARING_UPDATES_FRAGMENT_VIEWED,
287 sessionId,
288 numberOfAppUpdates
289 )
290 Log.i(
291 LOG_TAG,
292 "AppDataSharingUpdatesFragment viewed with" +
293 " sessionId=$sessionId" +
294 " numberOfAppUpdates=$numberOfAppUpdates"
295 )
296 }
297
298 private fun logAppDataSharingUpdatesFragmentActionReported(
299 sessionId: Long,
300 app: Application,
301 updateUiInfo: AppLocationDataSharingUpdateUiInfo
302 ) {
303 val uid: Int =
304 KotlinUtils.getPackageUid(app, updateUiInfo.packageName, updateUiInfo.userHandle)
305 ?: return
306 val dataSharingChangeValue: Int =
307 getStatsLogValueForLocationUpdateType(updateUiInfo.dataSharingUpdateType)
308 PermissionControllerStatsLog.write(
309 APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED,
310 sessionId,
311 uid,
312 dataSharingChangeValue
313 )
314 }
315
316 private fun getStatsLogValueForLocationUpdateType(type: DataSharingUpdateType): Int =
317 when (type) {
318 DataSharingUpdateType.ADDS_ADVERTISING_PURPOSE ->
319 APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_ADVERTISING_PURPOSE
320 DataSharingUpdateType.ADDS_SHARING_WITHOUT_ADVERTISING_PURPOSE ->
321 APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_SHARING_WITHOUT_ADVERTISING_PURPOSE
322 DataSharingUpdateType.ADDS_SHARING_WITH_ADVERTISING_PURPOSE ->
323 APP_DATA_SHARING_UPDATES_FRAGMENT_ACTION_REPORTED__DATA_SHARING_CHANGE__ADDS_SHARING_WITH_ADVERTISING_PURPOSE
324 }
325 }
326 }
327