• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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