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