<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