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.permission.ui.model.v34
18 
19 import android.app.Activity
20 import android.app.Application
21 import android.content.ActivityNotFoundException
22 import android.content.Context
23 import android.content.Intent
24 import android.net.Uri
25 import android.os.Build
26 import android.os.Bundle
27 import android.os.Process
28 import android.util.Log
29 import androidx.annotation.RequiresApi
30 import androidx.lifecycle.ViewModel
31 import androidx.lifecycle.ViewModelProvider
32 import com.android.permissioncontroller.Constants
33 import com.android.permissioncontroller.PermissionControllerStatsLog
34 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED
35 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__HELP_CENTER
36 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__INSTALL_SOURCE
37 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_SETTINGS
38 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_VIEWED
39 import com.android.permissioncontroller.R
40 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
41 import com.android.permissioncontroller.permission.data.get
42 import com.android.permissioncontroller.permission.data.v34.SafetyLabelInfoLiveData
43 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity
44 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED
45 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT
46 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleActivity
47 import com.android.permissioncontroller.permission.utils.KotlinUtils
48 import com.android.permissioncontroller.permission.utils.KotlinUtils.getAppStoreIntent
49 import com.android.permissioncontroller.permission.utils.v34.SafetyLabelUtils
50 import com.android.settingslib.HelpUtils
51 
52 /**
53  * [ViewModel] for the [PermissionRationaleActivity]. Gets all information required safety label and
54  * links required to inform user of data sharing usages by the app when granting this permission
55  *
56  * @param app: The current application
57  * @param packageName: The packageName permissions are being requested for
58  * @param permissionGroupName: The permission group requested
59  * @param sessionId: A long to identify this session
60  * @param storedState: Previous state, if this activity was stopped and is being recreated
61  */
62 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
63 class PermissionRationaleViewModel(
64     private val app: Application,
65     private val packageName: String,
66     private val permissionGroupName: String,
67     private val sessionId: Long,
68     private val storedState: Bundle?
69 ) : ViewModel() {
70     private val user = Process.myUserHandle()
71     private val safetyLabelInfoLiveData = SafetyLabelInfoLiveData[packageName, user]
72 
73     /** Interface for forwarding onActivityResult to this view model */
74     interface ActivityResultCallback {
75         /**
76          * Should be invoked by base activity when a valid onActivityResult is received
77          *
78          * @param data [Intent] which may contain result data from a started Activity (various data
79          *   can be attached to Intent "extras")
80          * @return {@code true} if Activity should finish after processing this result
81          */
82         fun shouldFinishActivityForResult(data: Intent?): Boolean
83     }
84     var activityResultCallback: ActivityResultCallback? = null
85 
86     /**
87      * A class which represents a permission rationale for permission group, and messages which
88      * should be shown with it.
89      */
90     data class PermissionRationaleInfo(
91         val groupName: String,
92         val isPreloadedApp: Boolean,
93         val installSourcePackageName: String?,
94         val installSourceLabel: String?,
95         val purposeSet: Set<Int>
96     )
97 
98     /** A [LiveData] which holds the currently pending PermissionRationaleInfo */
99     val permissionRationaleInfoLiveData =
100         object : SmartUpdateMediatorLiveData<PermissionRationaleInfo>() {
101 
102             init {
103                 addSource(safetyLabelInfoLiveData) { onUpdate() }
104 
105                 // Load package state, if available
106                 onUpdate()
107             }
108 
109             override fun onUpdate() {
110                 if (safetyLabelInfoLiveData.isStale) {
111                     return
112                 }
113 
114                 val safetyLabelInfo = safetyLabelInfoLiveData.value
115 
116                 if (safetyLabelInfo?.safetyLabel == null) {
117                     Log.e(LOG_TAG, "Safety label for $packageName not found")
118                     value = null
119                     return
120                 }
121 
122                 val installSourcePackageName =
123                     safetyLabelInfo.installSourceInfo.initiatingPackageName
124                 val installSourceLabel: String? =
125                     installSourcePackageName?.let {
126                         KotlinUtils.getPackageLabel(app, it, Process.myUserHandle())
127                     }
128 
129                 val purposes =
130                     SafetyLabelUtils.getSafetyLabelSharingPurposesForGroup(
131                         safetyLabelInfo.safetyLabel,
132                         permissionGroupName
133                     )
134                 if (value == null) {
135                     logPermissionRationaleDialogViewed(purposes)
136                 }
137                 value =
138                     PermissionRationaleInfo(
139                         permissionGroupName,
140                         safetyLabelInfo.installSourceInfo.isPreloadedApp,
141                         installSourcePackageName,
142                         installSourceLabel,
143                         purposes
144                     )
145             }
146         }
147 
148     fun canLinkToAppStore(context: Context, installSourcePackageName: String): Boolean {
149         return getAppStoreIntent(context, installSourcePackageName, packageName) != null
150     }
151 
152     fun sendToAppStore(context: Context, installSourcePackageName: String) {
153         logPermissionRationaleDialogActionReported(
154             PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__INSTALL_SOURCE
155         )
156         val storeIntent = getAppStoreIntent(context, installSourcePackageName, packageName)
157         context.startActivity(storeIntent)
158     }
159 
160     /**
161      * Send the user to the AppPermissionFragment
162      *
163      * @param activity The current activity
164      * @param groupName The name of the permission group whose fragment should be opened
165      */
166     fun sendToSettingsForPermissionGroup(activity: Activity, groupName: String) {
167         if (activityResultCallback != null) {
168             return
169         }
170         activityResultCallback =
171             object : ActivityResultCallback {
172                 override fun shouldFinishActivityForResult(data: Intent?): Boolean {
173                     val returnGroupName = data?.getStringExtra(EXTRA_RESULT_PERMISSION_INTERACTED)
174                     return (returnGroupName != null) &&
175                         data.hasExtra(EXTRA_RESULT_PERMISSION_RESULT)
176                 }
177             }
178         logPermissionRationaleDialogActionReported(
179             PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_SETTINGS
180         )
181         startAppPermissionFragment(activity, groupName)
182     }
183 
184     /** Returns whether UI can provide link to help center */
185     fun canLinkToHelpCenter(context: Context): Boolean {
186         return !getHelpCenterUrlString(context).isNullOrEmpty()
187     }
188 
189     /**
190      * Send the user to the Safety Label Android Help Center
191      *
192      * @param activity The current activity
193      */
194     fun sendToLearnMore(activity: Activity) {
195         if (!canLinkToHelpCenter(activity)) {
196             Log.w(LOG_TAG, "Unable to open help center, no url provided.")
197             return
198         }
199 
200         // Add in some extra locale query parameters
201         val fullUri =
202             HelpUtils.uriWithAddedParameters(activity, Uri.parse(getHelpCenterUrlString(activity)))
203         val intent =
204             Intent(Intent.ACTION_VIEW, fullUri).apply {
205                 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
206             }
207         logPermissionRationaleDialogActionReported(
208             PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__HELP_CENTER
209         )
210         try {
211             activity.startActivity(intent)
212         } catch (e: ActivityNotFoundException) {
213             // TODO(b/266755891): show snackbar when help center intent unable to be opened
214             Log.w(LOG_TAG, "Unable to open help center URL.", e)
215         }
216     }
217 
218     private fun startAppPermissionFragment(activity: Activity, groupName: String) {
219         val intent =
220             Intent(Intent.ACTION_MANAGE_APP_PERMISSION)
221                 .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
222                 .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName)
223                 .putExtra(Intent.EXTRA_USER, user)
224                 .putExtra(
225                     ManagePermissionsActivity.EXTRA_CALLER_NAME,
226                     PermissionRationaleActivity::class.java.name
227                 )
228                 .putExtra(Constants.EXTRA_SESSION_ID, sessionId)
229                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
230         activity.startActivityForResult(intent, APP_PERMISSION_REQUEST_CODE)
231     }
232 
233     private fun logPermissionRationaleDialogViewed(purposes: Set<Int>) {
234         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
235         var purposesPresented = 0
236         // Create bitmask for purposes presented, bit numbers are in accordance with PURPOSE_
237         // constants in [DataPurposeConstants]
238         purposes.forEach { purposeInt ->
239             purposesPresented = purposesPresented or 1.shl(purposeInt)
240         }
241         PermissionControllerStatsLog.write(
242             PERMISSION_RATIONALE_DIALOG_VIEWED,
243             sessionId,
244             uid,
245             permissionGroupName,
246             purposesPresented
247         )
248     }
249 
250     fun logPermissionRationaleDialogActionReported(buttonPressed: Int) {
251         val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return
252         PermissionControllerStatsLog.write(
253             PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED,
254             sessionId,
255             uid,
256             permissionGroupName,
257             buttonPressed
258         )
259     }
260 
261     private fun getHelpCenterUrlString(context: Context): String? {
262         return context.getString(R.string.data_sharing_help_center_link)
263     }
264 
265     companion object {
266         private val LOG_TAG = PermissionRationaleViewModel::class.java.simpleName
267 
268         const val APP_PERMISSION_REQUEST_CODE = 1
269     }
270 }
271 
272 /** Factory for a [PermissionRationaleViewModel] */
273 class PermissionRationaleViewModelFactory(
274     private val app: Application,
275     private val packageName: String,
276     private val permissionGroupName: String,
277     private val sessionId: Long,
278     private val savedState: Bundle?
279 ) : ViewModelProvider.Factory {
createnull280     override fun <T : ViewModel> create(modelClass: Class<T>): T {
281         @Suppress("UNCHECKED_CAST")
282         return PermissionRationaleViewModel(
283             app,
284             packageName,
285             permissionGroupName,
286             sessionId,
287             savedState
288         )
289             as T
290     }
291 }
292