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__INSTALL_SOURCE 36 import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__HELP_CENTER 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.v34.SafetyLabelInfoLiveData 41 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData 42 import com.android.permissioncontroller.permission.data.get 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 79 * (various data 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 = SafetyLabelUtils.getSafetyLabelSharingPurposesForGroup( 130 safetyLabelInfo.safetyLabel, permissionGroupName) 131 if (value == null) { 132 logPermissionRationaleDialogViewed(purposes) 133 } 134 value = 135 PermissionRationaleInfo( 136 permissionGroupName, 137 safetyLabelInfo.installSourceInfo.isPreloadedApp, 138 installSourcePackageName, 139 installSourceLabel, 140 purposes) 141 } 142 } 143 144 fun canLinkToAppStore(context: Context, installSourcePackageName: String): Boolean { 145 return getAppStoreIntent(context, installSourcePackageName, packageName) != null 146 } 147 148 fun sendToAppStore(context: Context, installSourcePackageName: String) { 149 logPermissionRationaleDialogActionReported( 150 PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__INSTALL_SOURCE) 151 val storeIntent = getAppStoreIntent(context, installSourcePackageName, packageName) 152 context.startActivity(storeIntent) 153 } 154 155 /** 156 * Send the user to the AppPermissionFragment 157 * 158 * @param activity The current activity 159 * @param groupName The name of the permission group whose fragment should be opened 160 */ 161 fun sendToSettingsForPermissionGroup(activity: Activity, groupName: String) { 162 if (activityResultCallback != null) { 163 return 164 } 165 activityResultCallback = object : ActivityResultCallback { 166 override fun shouldFinishActivityForResult(data: Intent?): Boolean { 167 val returnGroupName = data?.getStringExtra(EXTRA_RESULT_PERMISSION_INTERACTED) 168 return (returnGroupName != null) && data.hasExtra(EXTRA_RESULT_PERMISSION_RESULT) 169 } 170 } 171 logPermissionRationaleDialogActionReported( 172 PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__PERMISSION_SETTINGS) 173 startAppPermissionFragment(activity, groupName) 174 } 175 176 /** Returns whether UI can provide link to help center */ 177 fun canLinkToHelpCenter(context: Context): Boolean { 178 return !getHelpCenterUrlString(context).isNullOrEmpty() 179 } 180 181 /** 182 * Send the user to the Safety Label Android Help Center 183 * 184 * @param activity The current activity 185 */ 186 fun sendToLearnMore(activity: Activity) { 187 if (!canLinkToHelpCenter(activity)) { 188 Log.w(LOG_TAG, "Unable to open help center, no url provided.") 189 return 190 } 191 192 // Add in some extra locale query parameters 193 val fullUri = 194 HelpUtils.uriWithAddedParameters(activity, Uri.parse(getHelpCenterUrlString(activity))) 195 val intent = Intent(Intent.ACTION_VIEW, fullUri).apply { 196 setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) 197 } 198 logPermissionRationaleDialogActionReported( 199 PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED__BUTTON_PRESSED__HELP_CENTER) 200 try { 201 activity.startActivity(intent) 202 } catch (e: ActivityNotFoundException) { 203 // TODO(b/266755891): show snackbar when help center intent unable to be opened 204 Log.w(LOG_TAG, "Unable to open help center URL.", e) 205 } 206 } 207 208 private fun startAppPermissionFragment(activity: Activity, groupName: String) { 209 val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSION) 210 .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 211 .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName) 212 .putExtra(Intent.EXTRA_USER, user) 213 .putExtra(ManagePermissionsActivity.EXTRA_CALLER_NAME, 214 PermissionRationaleActivity::class.java.name) 215 .putExtra(Constants.EXTRA_SESSION_ID, sessionId) 216 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 217 activity.startActivityForResult(intent, APP_PERMISSION_REQUEST_CODE) 218 } 219 220 private fun logPermissionRationaleDialogViewed(purposes: Set<Int>) { 221 val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return 222 var purposesPresented = 0 223 // Create bitmask for purposes presented, bit numbers are in accordance with PURPOSE_ 224 // constants in [DataPurposeConstants] 225 purposes.forEach { purposeInt -> 226 purposesPresented = purposesPresented or 1.shl(purposeInt) 227 } 228 PermissionControllerStatsLog.write(PERMISSION_RATIONALE_DIALOG_VIEWED, sessionId, uid, 229 permissionGroupName, purposesPresented) 230 } 231 232 fun logPermissionRationaleDialogActionReported(buttonPressed: Int) { 233 val uid = KotlinUtils.getPackageUid(app, packageName, user) ?: return 234 PermissionControllerStatsLog.write(PERMISSION_RATIONALE_DIALOG_ACTION_REPORTED, sessionId, 235 uid, permissionGroupName, buttonPressed) 236 } 237 238 private fun getHelpCenterUrlString(context: Context): String? { 239 return context.getString(R.string.data_sharing_help_center_link) 240 } 241 242 companion object { 243 private val LOG_TAG = PermissionRationaleViewModel::class.java.simpleName 244 245 const val APP_PERMISSION_REQUEST_CODE = 1 246 } 247 } 248 249 /** Factory for a [PermissionRationaleViewModel] */ 250 class PermissionRationaleViewModelFactory( 251 private val app: Application, 252 private val packageName: String, 253 private val permissionGroupName: String, 254 private val sessionId: Long, 255 private val savedState: Bundle? 256 ) : ViewModelProvider.Factory { createnull257 override fun <T : ViewModel> create(modelClass: Class<T>): T { 258 @Suppress("UNCHECKED_CAST") 259 return PermissionRationaleViewModel( 260 app, packageName, permissionGroupName, sessionId, savedState) 261 as T 262 } 263 } 264