diff options
author | Nate Myren <ntmyren@google.com> | 2020-07-29 15:28:10 -0700 |
---|---|---|
committer | Nate Myren <ntmyren@google.com> | 2020-08-21 15:37:36 -0700 |
commit | e202d69020bcbfef3457de1a101a979535c40f56 (patch) | |
tree | a17882d78ad7762e4918aa8213b77b1b8e8160aa | |
parent | 5c2c1997d0fa96c6933b9b22702837e0877bffca (diff) | |
download | Permission-e202d69020bcbfef3457de1a101a979535c40f56.tar.gz |
Refactor GrantPermissionsActivity
Convert the GrantPermissionsActivity to the new LiveData and ViewModel
architecture. Also changes the SmartUpdateMediatorLiveData stale
behavior. Now, liveDatas will send the first non-stale update to all
observers, and the observeStale method has been removed.
Bug: 158311396
Test: atest CtsPermission3TestCases
Change-Id: I78ae7112022f3b828e4464076a3612df10d2c221
18 files changed, 1769 insertions, 1544 deletions
diff --git a/PermissionController/res/values/themes.xml b/PermissionController/res/values/themes.xml index 2a70fde21..4627fa4da 100644 --- a/PermissionController/res/values/themes.xml +++ b/PermissionController/res/values/themes.xml @@ -68,6 +68,24 @@ <item name="android:windowNoTitle">true</item> </style> + <style name="Theme.PermissionGrantDialog.Transparent" + parent="android:Theme.DeviceDefault.Settings"> + <item name="android:colorBackgroundCacheHint">@null</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="android:navigationBarDividerColor">@null</item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowAnimationStyle">@null</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowDisablePreview">true</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowLightNavigationBar">false</item> + <item name="android:windowLightStatusBar">false</item> + <item name="android:windowNoTitle">true</item> + + </style> + <style name="Theme.PermissionGrantDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"> <item name="android:divider">@drawable/list_divider</item> diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt index a916cf8e8..0022317f2 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/AppOpLiveData.kt @@ -30,8 +30,8 @@ import com.android.permissioncontroller.PermissionControllerApplication * * @see AppOpsManager */ -//TODO eugenesusla: observe appops -//TODO eugenesusla: use for external storage +// TODO eugenesusla: observe appops +// TODO eugenesusla: use for external storage class AppOpLiveData private constructor( private val app: Application, private val packageName: String, @@ -39,12 +39,17 @@ class AppOpLiveData private constructor( private val uid: Int ) : SmartUpdateMediatorLiveData<Int>() { - val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! + private val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! override fun onUpdate() { value = appOpsManager.unsafeCheckOpNoThrow(op, uid, packageName) } + override fun onActive() { + super.onActive() + updateIfActive() + } + /** * Repository for AppOpLiveData. * <p> Key value is a triple of string package name, string appop, and diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/CarrierPrivilegedStatusLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/CarrierPrivilegedStatusLiveData.kt index d87906217..04c43f5a7 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/CarrierPrivilegedStatusLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/CarrierPrivilegedStatusLiveData.kt @@ -37,6 +37,11 @@ class CarrierPrivilegedStatusLiveData private constructor( value = telephonyManager.checkCarrierPrivilegesForPackageAnyPhone(packageName) } + override fun onActive() { + super.onActive() + updateIfActive() + } + /** * Repository for [CarrierPrivilegedStatusLiveData]. * <p> Key value is a package name, value is its corresponding LiveData of diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/ForegroundPermNamesLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/ForegroundPermNamesLiveData.kt index aa4a8b9f9..7120147dd 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/ForegroundPermNamesLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/ForegroundPermNamesLiveData.kt @@ -23,7 +23,7 @@ import com.android.permissioncontroller.permission.utils.Utils * installed, runtime permission in every platform permission group. This LiveData's value is * static, since the background/foreground permission relationships are defined by the system. */ -object ForegroundPermNamesLiveData : SmartUpdateMediatorLiveData<Map<String, List<String>>>() { +object ForegroundPermNamesLiveData : SmartUpdateMediatorLiveData<Map<String, List<String>>>(true) { // Since the value will be static, initialize the value upon creating the LiveData. init { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/LightAppPermGroupLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/LightAppPermGroupLiveData.kt index 8ff5eaa33..5d94588fd 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/LightAppPermGroupLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/LightAppPermGroupLiveData.kt @@ -190,11 +190,11 @@ class LightAppPermGroupLiveData private constructor( } override fun onInactive() { - super.onInactive() - if (isSpecialLocation) { LocationUtils.removeLocationListener(this) } + + super.onInactive() } /** diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt index 84826c00a..956e373d1 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/SmartUpdateMediatorLiveData.kt @@ -18,11 +18,6 @@ package com.android.permissioncontroller.permission.data import android.util.Log import androidx.annotation.MainThread -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Lifecycle.State -import androidx.lifecycle.Lifecycle.State.STARTED -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.Observer @@ -39,8 +34,8 @@ import kotlinx.coroutines.launch * its value (avoiding unnecessary updates), and can calculate the set difference between a list * and a map (used when determining whether or not to add a LiveData as a source). */ -abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), - DataRepository.InactiveTimekeeper { +abstract class SmartUpdateMediatorLiveData<T>(private val isStaticVal: Boolean = false) + : MediatorLiveData<T>(), DataRepository.InactiveTimekeeper { companion object { const val DEBUG_UPDATES = false @@ -62,13 +57,8 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), var isStale = true private set - private val staleObservers = mutableListOf<Pair<LifecycleOwner, Observer<in T>>>() - private val sources = mutableListOf<SmartUpdateMediatorLiveData<*>>() - private val children = - mutableListOf<Triple<SmartUpdateMediatorLiveData<*>, Observer<in T>, Boolean>>() - private val stacktraceExceptionMessage = "Caller of coroutine" @MainThread @@ -77,31 +67,21 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), if (!isInitialized) { isInitialized = true - isStale = false // If we have received an invalid value, and this is the first time we are set, // notify observers. if (newValue == null) { + isStale = false super.setValue(newValue) return } } - if (valueNotEqual(super.getValue(), newValue)) { - isStale = false - super.setValue(newValue) - } else if (isStale) { - isStale = false - // We are no longer stale- notify active stale observers we are up-to-date - val liveObservers = staleObservers.filter { it.first.lifecycle.currentState >= STARTED } - for ((_, observer) in liveObservers) { - observer.onChanged(newValue) - } + val wasStale = isStale + // If all sources are not stale, and we are active, we are not stale + isStale = sources.any { it.isStale } || !hasActiveObservers() - for ((liveData, observer, shouldUpdate) in children.toList()) { - if (liveData.hasActiveObservers() && shouldUpdate) { - observer.onChanged(newValue) - } - } + if (valueNotEqual(super.getValue(), newValue) || (wasStale && !isStale)) { + super.setValue(newValue) } } @@ -136,18 +116,6 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), return valOne != valTwo } - @MainThread - fun observeStale(owner: LifecycleOwner, observer: Observer<in T>) { - val oldStaleObserver = hasStaleObserver() - staleObservers.add(owner to observer) - if (owner == ForeverActiveLifecycle) { - observeForever(observer) - } else { - observe(owner, observer) - } - updateSourceStaleObservers(oldStaleObserver, true) - } - override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { addSourceWithError(source, onChanged) } @@ -165,8 +133,6 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), if (source in sources) { return@launch } - source.addChild(this@SmartUpdateMediatorLiveData, onChanged, - staleObservers.isNotEmpty() || children.any { it.third }) sources.add(source) } try { @@ -180,7 +146,6 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), override fun <S : Any?> removeSource(toRemote: LiveData<S>) { GlobalScope.launch(Main.immediate) { if (toRemote is SmartUpdateMediatorLiveData) { - toRemote.removeChild(this@SmartUpdateMediatorLiveData) sources.remove(toRemote) } super.removeSource(toRemote) @@ -240,81 +205,22 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), } } - @MainThread - private fun <S : Any?> removeChild(liveData: LiveData<S>) { - children.removeIf { it.first == liveData } - } - - @MainThread - private fun <S : Any?> addChild( - liveData: SmartUpdateMediatorLiveData<S>, - onChanged: Observer<in T>, - sendStaleUpdates: Boolean - ) { - children.add(Triple(liveData, onChanged, sendStaleUpdates)) - } - - @MainThread - private fun <S : Any?> updateShouldSendStaleUpdates( - liveData: SmartUpdateMediatorLiveData<S>, - sendStaleUpdates: Boolean - ) { - for ((idx, childTriple) in children.withIndex()) { - if (childTriple.first == liveData) { - children[idx] = Triple(liveData, childTriple.second, sendStaleUpdates) - } - } - } - - @MainThread - override fun removeObserver(observer: Observer<in T>) { - val oldStaleObserver = hasStaleObserver() - staleObservers.removeIf { it.second == observer } - super.removeObserver(observer) - updateSourceStaleObservers(oldStaleObserver, hasStaleObserver()) - } - - @MainThread - override fun removeObservers(owner: LifecycleOwner) { - val oldStaleObserver = hasStaleObserver() - staleObservers.removeIf { it.first == owner } - super.removeObservers(owner) - updateSourceStaleObservers(oldStaleObserver, hasStaleObserver()) - } - - @MainThread - override fun observeForever(observer: Observer<in T>) { - super.observeForever(observer) - } - - @MainThread - private fun updateSourceStaleObservers(hadStaleObserver: Boolean, hasStaleObserver: Boolean) { - if (hadStaleObserver == hasStaleObserver) { - return - } - for (liveData in sources) { - liveData.updateShouldSendStaleUpdates(this, hasStaleObserver) - } - - // if all sources are not stale, and we just requested stale updates, and we are stale, - // update our value - if (sources.all { !it.isStale } && hasStaleObserver && isStale) { - updateIfActive() - } - } - - private fun hasStaleObserver(): Boolean { - return staleObservers.isNotEmpty() || children.any { it.third } - } - override fun onActive() { timeWentInactive = null + // If this is not an async livedata, and we have sources, and all sources are non-stale, + // force update our value + if (sources.isNotEmpty() && sources.all { !it.isStale } && + this !is SmartAsyncMediatorLiveData<T>) { + updateIfActive() + } super.onActive() } override fun onInactive() { timeWentInactive = System.nanoTime() - isStale = true + if (!isStaticVal) { + isStale = true + } super.onInactive() } @@ -327,29 +233,11 @@ abstract class SmartUpdateMediatorLiveData<T> : MediatorLiveData<T>(), suspend fun getInitializedValue(staleOk: Boolean = false, forceUpdate: Boolean = false): T { return getInitializedValue( observe = { observer -> - observeStale(ForeverActiveLifecycle, observer) + observeForever(observer) if (forceUpdate) { updateIfActive() } }, isInitialized = { isInitialized && (staleOk || !isStale) }) } - - /** - * A [Lifecycle]/[LifecycleOwner] that is permanently [State.STARTED] - * - * Passing this to [LiveData.observe] is essentially equivalent to using - * [LiveData.observeForever], so you have to make sure you handle your own cleanup whenever - * using this. - */ - private object ForeverActiveLifecycle : Lifecycle(), LifecycleOwner { - - override fun getLifecycle(): Lifecycle = this - - override fun addObserver(observer: LifecycleObserver) {} - - override fun removeObserver(observer: LifecycleObserver) {} - - override fun getCurrentState(): State = State.STARTED - } }
\ No newline at end of file diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightAppPermGroup.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightAppPermGroup.kt index ef941054f..5a89be61b 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightAppPermGroup.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightAppPermGroup.kt @@ -16,6 +16,7 @@ package com.android.permissioncontroller.permission.model.livedatatypes +import android.content.pm.PermissionInfo import android.os.Build import android.os.UserHandle @@ -78,10 +79,10 @@ data class LightAppPermGroup( } val foreground = AppPermSubGroup(permissions.filter { it.key in foregroundPermNames }, - specialLocationGrant) + packageInfo, specialLocationGrant) val background = AppPermSubGroup(permissions.filter { it.key in backgroundPermNames }, - specialLocationGrant) + packageInfo, specialLocationGrant) /** * Whether or not this App Permission Group has a permission which has a background mode @@ -144,6 +145,7 @@ data class LightAppPermGroup( */ data class AppPermSubGroup internal constructor( private val permissions: Map<String, LightPermission>, + private val packageInfo: LightPackageInfo, private val specialLocationGrant: Boolean? ) { /** @@ -180,5 +182,12 @@ data class LightAppPermGroup( * Whether any of this App Permission Subgroup's permissions are set by the role of this app */ val isGrantedByRole = permissions.any { it.value.isGrantedByRole } + + private val hasPreRuntimePerm = permissions.any { (_, perm) -> + perm.permInfo.protectionFlags and PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY == 0 + } + + val isGrantable = !packageInfo.isInstantApp && + (packageInfo.targetSdkVersion >= Build.VERSION_CODES.M || hasPreRuntimePerm) } }
\ No newline at end of file diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/PermissionControllerServiceModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/service/PermissionControllerServiceModel.kt index ee4450201..a32855505 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/service/PermissionControllerServiceModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/service/PermissionControllerServiceModel.kt @@ -92,11 +92,7 @@ class PermissionControllerServiceModel(private val service: PermissionController } } - if (liveData is SmartUpdateMediatorLiveData<T>) { - liveData.observeStale(service, observer) - } else { - liveData.observe(service, observer) - } + liveData.observe(service, observer) } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java index f7ae2d244..289403685 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/GrantPermissionsActivity.java @@ -16,24 +16,8 @@ package com.android.permissioncontroller.permission.ui; -import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; -import static com.android.permissioncontroller.PermissionControllerStatsLog.GRANT_PERMISSIONS_ACTIVITY_BUTTON_ACTIONS; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_DENIED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_POLICY_FIXED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_RESTRICTED_PERMISSION; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_USER_FIXED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE_IN_SETTINGS; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_ONE_TIME; -import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED; import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.CANCELED; import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED; import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN; @@ -41,67 +25,52 @@ import static com.android.permissioncontroller.permission.ui.GrantPermissionsVie import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY; import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ONE_TIME; import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.LINKED_TO_SETTINGS; -import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME; -import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED; -import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT; import static com.android.permissioncontroller.permission.utils.Utils.getRequestMessage; -import android.Manifest; -import android.app.Activity; import android.app.KeyguardManager; -import android.app.admin.DevicePolicyManager; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Icon; -import android.os.Build; import android.os.Bundle; -import android.os.UserHandle; -import android.permission.PermissionManager; +import android.os.Process; import android.text.Annotation; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ClickableSpan; -import android.util.ArrayMap; -import android.util.ArraySet; import android.util.Log; -import android.util.Pair; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.util.Consumer; +import androidx.fragment.app.FragmentActivity; -import com.android.permissioncontroller.Constants; import com.android.permissioncontroller.DeviceUtils; -import com.android.permissioncontroller.PermissionControllerStatsLog; import com.android.permissioncontroller.R; -import com.android.permissioncontroller.permission.model.AppPermissionGroup; -import com.android.permissioncontroller.permission.model.AppPermissions; -import com.android.permissioncontroller.permission.model.Permission; import com.android.permissioncontroller.permission.ui.auto.GrantPermissionsAutoViewHandler; +import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModel; +import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModel.RequestInfo; +import com.android.permissioncontroller.permission.ui.model.GrantPermissionsViewModelFactory; import com.android.permissioncontroller.permission.ui.wear.GrantPermissionsWearViewHandler; -import com.android.permissioncontroller.permission.utils.ArrayUtils; -import com.android.permissioncontroller.permission.utils.PackageRemovalMonitor; -import com.android.permissioncontroller.permission.utils.SafetyNetLogger; +import com.android.permissioncontroller.permission.utils.KotlinUtils; import com.android.permissioncontroller.permission.utils.Utils; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Random; -public class GrantPermissionsActivity extends Activity +/** + * An activity which displays runtime permission prompts on behalf of an app. + */ +public class GrantPermissionsActivity extends FragmentActivity implements GrantPermissionsViewHandler.ResultListener { - private static final String LOG_TAG = "GrantPermissionsActivity"; + private static final String LOG_TAG = "GrantPermissionsActivit"; - private static final String KEY_REQUEST_ID = GrantPermissionsActivity.class.getName() + private static final String KEY_SESSION_ID = GrantPermissionsActivity.class.getName() + "_REQUEST_ID"; public static final String ANNOTATION_ID = "link"; @@ -121,191 +90,43 @@ public class GrantPermissionsActivity extends Activity private static final int APP_PERMISSION_REQUEST_CODE = 1; /** Unique Id of a request */ - private long mRequestId; + private long mSessionId; private String[] mRequestedPermissions; private boolean[] mButtonVisibilities; - private boolean mCouldHaveFgCapabilities; - - private ArrayMap<Pair<String, Boolean>, GroupState> mRequestGrantPermissionGroups = - new ArrayMap<>(); - private ArraySet<String> mPermissionGroupsToSkip = new ArraySet<>(); - private Consumer<Intent> mActivityResultCallback; - + private List<RequestInfo> mRequestInfos = new ArrayList<>(); private GrantPermissionsViewHandler mViewHandler; - private AppPermissions mAppPermissions; - - boolean mResultSet; - - /** - * Listens for changes to the permission of the app the permissions are currently getting - * granted to. {@code null} when unregistered. - */ - private @Nullable PackageManager.OnPermissionsChangedListener mPermissionChangeListener; - - /** - * Listens for changes to the app the permissions are currently getting granted to. {@code null} - * when unregistered. - */ - private @Nullable PackageRemovalMonitor mPackageRemovalMonitor; - + private GrantPermissionsViewModel mViewModel; + private boolean mResultSet; /** Package that requested the permission grant */ private String mCallingPackage; - /** uid of {@link #mCallingPackage} */ - private int mCallingUid; - /** Notifier for auto-granted permissions */ - private AutoGrantPermissionsNotifier mAutoGrantPermissionsNotifier; - private PackageInfo mCallingPackageInfo; - - private int getPermissionPolicy() { - DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class); - return devicePolicyManager.getPermissionPolicy(null); - } - - /** - * Try to add a single permission that is requested to be granted. - * - * <p>This does <u>not</u> expand the permissions into the {@link #computeAffectedPermissions - * affected permissions}. - * - * @param group The group the permission belongs to (might be a background permission group) - * @param permName The name of the permission to add - * @param isFirstInstance Is this the first time the groupStates get created - */ - private void addRequestedPermissions(AppPermissionGroup group, String permName, - boolean isFirstInstance) { - if (!group.isGrantingAllowed()) { - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED); - // Skip showing groups that we know cannot be granted. - return; - } - - Permission permission = group.getPermission(permName); - - // If the permission is restricted it does not show in the UI and - // is not added to the group at all, so check that first. - if (permission == null && ArrayUtils.contains( - mAppPermissions.getPackageInfo().requestedPermissions, permName)) { - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_RESTRICTED_PERMISSION); - return; - // We allow the user to choose only non-fixed permissions. A permission - // is fixed either by device policy or the user denying with prejudice. - } else if (group.isUserFixed()) { - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_USER_FIXED); - return; - } else if (group.isPolicyFixed() && !group.areRuntimePermissionsGranted() - || permission.isPolicyFixed()) { - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_POLICY_FIXED); - return; - } - - Pair<String, Boolean> groupKey = new Pair<>(group.getName(), - group.isBackgroundGroup()); - - GroupState state = mRequestGrantPermissionGroups.get(groupKey); - if (state == null) { - state = new GroupState(group); - mRequestGrantPermissionGroups.put(groupKey, state); - } - state.affectedPermissions = ArrayUtils.appendString( - state.affectedPermissions, permName); - - boolean skipGroup = false; - switch (getPermissionPolicy()) { - case DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT: { - final String[] filterPermissions = new String[]{permName}; - group.grantRuntimePermissions(false, false, filterPermissions); - group.setPolicyFixed(filterPermissions); - state.mState = GroupState.STATE_ALLOWED; - skipGroup = true; - - getAutoGrantNotifier().onPermissionAutoGranted(permName); - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED); - } break; - - case DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY: { - final String[] filterPermissions = new String[]{permName}; - group.setPolicyFixed(filterPermissions); - state.mState = GroupState.STATE_DENIED; - skipGroup = true; - - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_DENIED); - } break; - - default: { - if (group.areRuntimePermissionsGranted()) { - group.grantRuntimePermissions(false, false, new String[]{permName}); - state.mState = GroupState.STATE_ALLOWED; - skipGroup = true; - - reportRequestResult(permName, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED); - } - } break; - } - - if (skipGroup && isFirstInstance) { - // Only allow to skip groups when this is the first time the dialog was created. - // Otherwise the number of groups changes between instances of the dialog. - state.mState = GroupState.STATE_SKIPPED; - } - } - - /** - * Report the result of a grant of a permission. - * - * @param permission The permission that was granted or denied - * @param result The permission grant result - */ - private void reportRequestResult(@NonNull String permission, int result) { - boolean isImplicit = !ArrayUtils.contains(mRequestedPermissions, permission); - - Log.v(LOG_TAG, - "Permission grant result requestId=" + mRequestId + " callingUid=" + mCallingUid - + " callingPackage=" + mCallingPackage + " permission=" + permission - + " isImplicit=" + isImplicit + " result=" + result); - - PermissionControllerStatsLog.write( - PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED, mRequestId, - mCallingUid, mCallingPackage, permission, isImplicit, result); - } - - /** - * Report the result of a grant of a permission. - * - * @param permissions The permissions that were granted or denied - * @param result The permission grant result - */ - private void reportRequestResult(@NonNull String[] permissions, int result) { - for (String permission : permissions) { - reportRequestResult(permission, result); - } - } + private int mTotalRequests = 0; + private int mCurrentRequestIdx = 0; + private Resources.Theme mOriginalTheme; + private View mRootView; @Override public void onCreate(Bundle icicle) { + mOriginalTheme = getTheme(); + // Set the activity to be transparent until data is loaded + setTheme(R.style.Theme_PermissionGrantDialog_Transparent); super.onCreate(icicle); if (icicle == null) { - mRequestId = new Random().nextLong(); + mSessionId = new Random().nextLong(); } else { - mRequestId = icicle.getLong(KEY_REQUEST_ID); + mSessionId = icicle.getLong(KEY_SESSION_ID); } getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); // Cache this as this can only read on onCreate, not later. mCallingPackage = getCallingPackage(); - try { - mCouldHaveFgCapabilities = Utils.couldHaveForegroundCapabilities(this, mCallingPackage); - } catch (NameNotFoundException e) { - Log.e(LOG_TAG, "Calling package " + mCallingPackage + " not found", e); + if (mCallingPackage == null) { + Log.e(LOG_TAG, "null callingPackageName. Please use \"RequestPermission\" to " + + "request permissions"); + setResultAndFinish(); + return; } setFinishOnTouchOutside(false); @@ -314,39 +135,11 @@ public class GrantPermissionsActivity extends Activity mRequestedPermissions = getIntent().getStringArrayExtra( PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES); - if (mRequestedPermissions == null) { - mRequestedPermissions = new String[0]; - } - - final int requestedPermCount = mRequestedPermissions.length; - - if (requestedPermCount == 0) { + if (mRequestedPermissions == null || mRequestedPermissions.length == 0) { setResultAndFinish(); return; } - PackageInfo callingPackageInfo = getCallingPackageInfo(); - - if (callingPackageInfo == null || callingPackageInfo.requestedPermissions == null - || callingPackageInfo.requestedPermissions.length <= 0) { - setResultAndFinish(); - return; - } - - // Don't allow legacy apps to request runtime permissions. - if (callingPackageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.M) { - // Returning empty arrays means a cancellation. - mRequestedPermissions = new String[0]; - setResultAndFinish(); - return; - } - - mCallingPackageInfo = callingPackageInfo; - - mCallingUid = callingPackageInfo.applicationInfo.uid; - - UserHandle userHandle = UserHandle.getUserHandleForUid(mCallingUid); - if (DeviceUtils.isTelevision(this)) { mViewHandler = new com.android.permissioncontroller.permission.ui.television .GrantPermissionsViewHandlerImpl(this, @@ -358,216 +151,150 @@ public class GrantPermissionsActivity extends Activity .setResultListener(this); } else { mViewHandler = new com.android.permissioncontroller.permission.ui.handheld - .GrantPermissionsViewHandlerImpl(this, mCallingPackage, userHandle) - .setResultListener(this); + .GrantPermissionsViewHandlerImpl(this, mCallingPackage, + Process.myUserHandle()).setResultListener(this); } - mAppPermissions = new AppPermissions(this, callingPackageInfo, false, - new Runnable() { - @Override - public void run() { - setResultAndFinish(); - } - }); - - for (String requestedPermission : mRequestedPermissions) { - if (requestedPermission == null) { - continue; - } - - ArrayList<String> affectedPermissions = - computeAffectedPermissions(requestedPermission); + GrantPermissionsViewModelFactory factory = new GrantPermissionsViewModelFactory( + getApplication(), mCallingPackage, mRequestedPermissions, mSessionId, icicle); + mViewModel = factory.create(GrantPermissionsViewModel.class); + mViewModel.getRequestInfosLiveData().observe(this, this::onRequestInfoLoad); - int numAffectedPermissions = affectedPermissions.size(); - for (int i = 0; i < numAffectedPermissions; i++) { - AppPermissionGroup group = - mAppPermissions.getGroupForPermission(affectedPermissions.get(i)); - if (group == null) { - reportRequestResult(affectedPermissions.get(i), - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED); - - continue; - } - - if (mAppPermissions.getPackageInfo().applicationInfo.targetSdkVersion - >= Build.VERSION_CODES.R && mRequestedPermissions.length > 1 - && group.isBackgroundGroup()) { - Log.e(LOG_TAG, "Apps targeting " + Build.VERSION_CODES.R + " must" - + " have foreground permission before requesting background and must" - + " request background on its own."); - finish(); - } - - addRequestedPermissions(group, affectedPermissions.get(i), icicle == null); - } + // Restore UI state after lifecycle events. This has to be before we show the first request, + // as the UI behaves differently for updates and initial creations. + if (icicle != null) { + setUpView(); + mViewHandler.loadInstanceState(icicle); } + } - int numGroupStates = mRequestGrantPermissionGroups.size(); - for (int groupStateNum = 0; groupStateNum < numGroupStates; groupStateNum++) { - GroupState groupState = mRequestGrantPermissionGroups.valueAt(groupStateNum); - AppPermissionGroup group = groupState.mGroup; + private void setUpView() { + if (mRootView != null) { + return; + } - // Restore permission group state after lifecycle events - if (icicle != null) { - groupState.mState = icicle.getInt( - getInstanceStateKey(mRequestGrantPermissionGroups.keyAt(groupStateNum)), - groupState.mState); - } + setTheme(mOriginalTheme); - // Do not attempt to grant background access if foreground access is not either already - // granted or requested - if (group.isBackgroundGroup()) { - // Check if a foreground permission is already granted - boolean foregroundGroupAlreadyGranted = mAppPermissions.getPermissionGroup( - group.getName()).areRuntimePermissionsGranted(); - boolean hasForegroundRequest = (getForegroundGroupState(group.getName()) != null); - - if (!foregroundGroupAlreadyGranted && !hasForegroundRequest) { - // The background permission cannot be granted at this time - int numPermissions = groupState.affectedPermissions.length; - for (int permissionNum = 0; permissionNum < numPermissions; permissionNum++) { - Log.w(LOG_TAG, - "Cannot grant " + groupState.affectedPermissions[permissionNum] - + " as the matching foreground permission is not already " - + "granted."); - } - - groupState.mState = GroupState.STATE_SKIPPED; - - reportRequestResult(groupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED); - } - } - } + mRootView = mViewHandler.createView(); + mRootView.setVisibility(View.GONE); + setContentView(mRootView); - setContentView(mViewHandler.createView()); Window window = getWindow(); WindowManager.LayoutParams layoutParams = window.getAttributes(); mViewHandler.updateWindowAttributes(layoutParams); window.setAttributes(layoutParams); + } - // Restore UI state after lifecycle events. This has to be before - // showNextPermissionGroupGrantRequest is called. showNextPermissionGroupGrantRequest might - // update the UI and the UI behaves differently for updates and initial creations. - if (icicle != null) { - mViewHandler.loadInstanceState(icicle); - } - - if (!showNextPermissionGroupGrantRequest()) { + private void onRequestInfoLoad(List<RequestInfo> requests) { + if (!mViewModel.getRequestInfosLiveData().isInitialized() || mResultSet) { + return; + } else if (requests == null) { + finish(); + return; + } else if (requests.isEmpty()) { setResultAndFinish(); + return; } - } - /** - * Update the {@link #mRequestedPermissions} if the system reports them as granted. - * - * <p>This also updates the {@link #mAppPermissions} state and switches to the next group grant - * request if the current group becomes granted. - */ - private void updateIfPermissionsWereGranted() { - PackageManager pm = getPackageManager(); - - boolean mightShowNextGroup = true; - int numGroupStates = mRequestGrantPermissionGroups.size(); - for (int i = 0; i < numGroupStates; i++) { - GroupState groupState = mRequestGrantPermissionGroups.valueAt(i); - - if (groupState == null || groupState.mState != GroupState.STATE_UNKNOWN) { - // Group has already been approved / denied via the UI by the user - continue; - } - - boolean allAffectedPermissionsOfThisGroupAreGranted = true; - - if (groupState.affectedPermissions == null) { - // It is not clear which permissions belong to this group, hence never skip this - // view - allAffectedPermissionsOfThisGroupAreGranted = false; - } else { - for (int permNum = 0; permNum < groupState.affectedPermissions.length; - permNum++) { - if (pm.checkPermission(groupState.affectedPermissions[permNum], mCallingPackage) - == PERMISSION_DENIED) { - allAffectedPermissionsOfThisGroupAreGranted = false; - break; - } - } - } + setUpView(); - if (allAffectedPermissionsOfThisGroupAreGranted) { - groupState.mState = GroupState.STATE_ALLOWED; - - if (mightShowNextGroup) { - // The UI currently displays the first group with - // mState == STATE_UNKNOWN. So we are switching to next group until we - // could not allow a group that was still unknown - if (!showNextPermissionGroupGrantRequest()) { - setResultAndFinish(); - } - } - } else { - mightShowNextGroup = false; - } + if (mRequestInfos == null) { + mTotalRequests = requests.size(); } - } + mRequestInfos = requests; - @Override - protected void onStart() { - super.onStart(); + showNextRequest(); + } - try { - mPermissionChangeListener = new PermissionChangeListener(); - } catch (NameNotFoundException e) { - setResultAndFinish(); + private void showNextRequest() { + if (mRequestInfos == null || mRequestInfos.isEmpty()) { return; } - PackageManager pm = getPackageManager(); - pm.addOnPermissionsChangeListener(mPermissionChangeListener); - // get notified when the package is removed - mPackageRemovalMonitor = new PackageRemovalMonitor(this, mCallingPackage) { - @Override - public void onPackageRemoved() { - Log.w(LOG_TAG, mCallingPackage + " was uninstalled"); + RequestInfo info = mRequestInfos.get(0); - finish(); - } - }; - mPackageRemovalMonitor.register(); - - // check if the package was removed while this activity was not started - try { - pm.getPackageInfo(mCallingPackage, 0); - } catch (NameNotFoundException e) { - Log.w(LOG_TAG, mCallingPackage + " was uninstalled while this activity was stopped", e); - finish(); + if (info.getSendToSettingsImmediately()) { + mViewModel.sendDirectlyToSettings(this, info.getGroupName()); + return; } - updateIfPermissionsWereGranted(); - } + CharSequence appLabel = KotlinUtils.INSTANCE.getPackageLabel(getApplication(), + mCallingPackage, Process.myUserHandle()); - @Override - protected void onResume() { - if (!showNextPermissionGroupGrantRequest()) { - setResultAndFinish(); + int messageId = 0; + switch(info.getMessage()) { + case FG_MESSAGE: + messageId = Utils.getRequest(info.getGroupName()); + break; + case BG_MESSAGE: + messageId = Utils.getBackgroundRequest(info.getGroupName()); + break; + case UPGRADE_MESSAGE: + messageId = Utils.getUpgradeRequest(info.getGroupName()); } - super.onResume(); - } - @Override - protected void onStop() { - super.onStop(); + CharSequence message = getRequestMessage(appLabel, mCallingPackage, + info.getGroupName(), this, messageId); - if (mPackageRemovalMonitor != null) { - mPackageRemovalMonitor.unregister(); - mPackageRemovalMonitor = null; + int detailMessageId = 0; + switch(info.getDetailMessage()) { + case FG_MESSAGE: + detailMessageId = Utils.getRequestDetail(info.getGroupName()); + break; + case BG_MESSAGE: + detailMessageId = Utils.getBackgroundRequestDetail(info.getGroupName()); + break; + case UPGRADE_MESSAGE: + detailMessageId = Utils.getUpgradeRequestDetail(info.getGroupName()); + } + + Spanned detailMessage = null; + if (detailMessageId != 0) { + detailMessage = + new SpannableString(getText(detailMessageId)); + Annotation[] annotations = detailMessage.getSpans( + 0, detailMessage.length(), Annotation.class); + int numAnnotations = annotations.length; + for (int i = 0; i < numAnnotations; i++) { + Annotation annotation = annotations[i]; + if (annotation.getValue().equals(ANNOTATION_ID)) { + int start = detailMessage.getSpanStart(annotation); + int end = detailMessage.getSpanEnd(annotation); + ClickableSpan clickableSpan = getLinkToAppPermissions(info); + SpannableString spannableString = + new SpannableString(detailMessage); + spannableString.setSpan(clickableSpan, start, end, 0); + detailMessage = spannableString; + break; + } + } } - if (mPermissionChangeListener != null) { - getPackageManager().removeOnPermissionsChangeListener(mPermissionChangeListener); - mPermissionChangeListener = null; + Icon icon = null; + try { + icon = Icon.createWithResource(info.getGroupInfo().getPackageName(), + info.getGroupInfo().getIcon()); + } catch (Resources.NotFoundException e) { + Log.e(LOG_TAG, "Cannot load icon for group" + info.getGroupName(), e); + } + + boolean showingNewGroup = message == null || !message.equals(getTitle()); + setTitle(message); + ArrayList<Integer> idxs = new ArrayList<>(); + mButtonVisibilities = new boolean[info.getButtonVisibilities().size()]; + for (int i = 0; i < info.getButtonVisibilities().size(); i++) { + mButtonVisibilities[i] = info.getButtonVisibilities().get(i); + if (mButtonVisibilities[i]) { + idxs.add(i); + } } + mViewHandler.updateUi(info.getGroupName(), mTotalRequests, mCurrentRequestIdx, icon, + message, detailMessage, mButtonVisibilities); + if (showingNewGroup) { + mCurrentRequestIdx++; + } + mRootView.setVisibility(View.VISIBLE); } @Override @@ -580,423 +307,42 @@ public class GrantPermissionsActivity extends Activity return super.dispatchTouchEvent(ev); } - /** - * Compose a key that stores the GroupState.mState in the instance state. - * - * @param requestGrantPermissionGroupsKey The key of the permission group - * - * @return A unique key to be used in the instance state - */ - private static String getInstanceStateKey( - Pair<String, Boolean> requestGrantPermissionGroupsKey) { - return GrantPermissionsActivity.class.getName() + "_" - + requestGrantPermissionGroupsKey.first + "_" - + requestGrantPermissionGroupsKey.second; - } - @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); mViewHandler.saveInstanceState(outState); + mViewModel.saveInstanceState(outState); - outState.putLong(KEY_REQUEST_ID, mRequestId); - - int numGroups = mRequestGrantPermissionGroups.size(); - for (int i = 0; i < numGroups; i++) { - int state = mRequestGrantPermissionGroups.valueAt(i).mState; - - if (state != GroupState.STATE_UNKNOWN) { - outState.putInt(getInstanceStateKey(mRequestGrantPermissionGroups.keyAt(i)), state); - } - } - } - - /** - * @return the background group state for the permission group with the {@code name} - */ - private GroupState getBackgroundGroupState(String name) { - return mRequestGrantPermissionGroups.get(new Pair<>(name, true)); - } - - /** - * @return the foreground group state for the permission group with the {@code name} - */ - private GroupState getForegroundGroupState(String name) { - return mRequestGrantPermissionGroups.get(new Pair<>(name, false)); - } - - private boolean shouldShowRequestForGroupState(GroupState groupState) { - if (groupState.mState == GroupState.STATE_SKIPPED) { - return false; - } - - GroupState foregroundGroup = getForegroundGroupState(groupState.mGroup.getName()); - if (groupState.mGroup.isBackgroundGroup() - && (foregroundGroup != null && shouldShowRequestForGroupState(foregroundGroup))) { - // If an app requests both foreground and background permissions of the same group, - // we only show one request - return false; - } - - return !mPermissionGroupsToSkip.contains(groupState.mGroup.getName()); + outState.putLong(KEY_SESSION_ID, mSessionId); } - private boolean showNextPermissionGroupGrantRequest() { - int numGroupStates = mRequestGrantPermissionGroups.size(); - int numGrantRequests = 0; - for (int i = 0; i < numGroupStates; i++) { - if (shouldShowRequestForGroupState(mRequestGrantPermissionGroups.valueAt(i))) { - numGrantRequests++; - } - } - - int currentIndex = 0; - List<GroupState> groupStates = new ArrayList<>(mRequestGrantPermissionGroups.values()); - Collections.sort(groupStates, (s1, s2) -> -Boolean.compare(s1.mGroup.supportsOneTimeGrant(), - s2.mGroup.supportsOneTimeGrant())); - for (GroupState groupState : groupStates) { - if (!shouldShowRequestForGroupState(groupState)) { - continue; - } - - if (groupState.mState == GroupState.STATE_UNKNOWN) { - GroupState foregroundGroupState; - GroupState backgroundGroupState; - if (groupState.mGroup.isBackgroundGroup()) { - backgroundGroupState = groupState; - foregroundGroupState = getForegroundGroupState(groupState.mGroup.getName()); - } else { - foregroundGroupState = groupState; - backgroundGroupState = getBackgroundGroupState(groupState.mGroup.getName()); - } - - CharSequence appLabel = mAppPermissions.getAppLabel(); - - Icon icon; - try { - icon = Icon.createWithResource(groupState.mGroup.getIconPkg(), - groupState.mGroup.getIconResId()); - } catch (Resources.NotFoundException e) { - Log.e(LOG_TAG, "Cannot load icon for group" + groupState.mGroup.getName(), e); - icon = null; - } - - // If no background permissions are granted yet, we need to ask for background - // permissions - boolean needBackgroundPermission = false; - boolean isBackgroundPermissionUserSet = false; - if (backgroundGroupState != null) { - if (!backgroundGroupState.mGroup.areRuntimePermissionsGranted()) { - needBackgroundPermission = true; - isBackgroundPermissionUserSet = backgroundGroupState.mGroup.isUserSet(); - } - } - - // If no foreground permissions are granted yet, we need to ask for foreground - // permissions - boolean needForegroundPermission = false; - boolean isForegroundPermissionUserSet = false; - if (foregroundGroupState != null) { - if (!foregroundGroupState.mGroup.areRuntimePermissionsGranted()) { - needForegroundPermission = true; - isForegroundPermissionUserSet = foregroundGroupState.mGroup.isUserSet(); - } - } - - mButtonVisibilities = new boolean[NEXT_BUTTON]; - mButtonVisibilities[ALLOW_BUTTON] = true; - mButtonVisibilities[DENY_BUTTON] = true; - mButtonVisibilities[ALLOW_ONE_TIME_BUTTON] = - groupState.mGroup.supportsOneTimeGrant(); - - int messageId; - int detailMessageId = 0; - - if (mAppPermissions.getPackageInfo().applicationInfo.targetSdkVersion - >= Build.VERSION_CODES.R) { - if (groupState.mGroup.hasPermissionWithBackgroundMode() - || groupState.mGroup.isBackgroundGroup()) { - if (needForegroundPermission && needBackgroundPermission) { - if (mCouldHaveFgCapabilities) { - sendToSettings(groupState); - return true; - } - // Shouldn't be reached as background must be requested as a singleton - return false; - } else if (needForegroundPermission) { - // Case: sdk >= R, BG/FG permission requesting FG only - messageId = groupState.mGroup.getRequest(); - if (mCouldHaveFgCapabilities) { - sendToSettings(groupState); - return true; - } - mButtonVisibilities[ALLOW_BUTTON] = false; - mButtonVisibilities[ALLOW_FOREGROUND_BUTTON] = true; - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - } else if (needBackgroundPermission) { - // Case: sdk >= R, BG/FG permission requesting BG only - sendToSettings(groupState); - return true; - } else { - // Not reached as the permissions should be auto-granted - return false; - } - } else { - // Case: sdk >= R, Requesting normal permission - messageId = groupState.mGroup.getRequest(); - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - if (groupState.mGroup.getName().equals(Manifest.permission_group.CAMERA) - || groupState.mGroup.getName().equals( - Manifest.permission_group.MICROPHONE)) { - mButtonVisibilities[ALLOW_BUTTON] = false; - if (mCouldHaveFgCapabilities - || Utils.isEmergencyApp(this, mCallingPackage)) { - mButtonVisibilities[ALLOW_ALWAYS_BUTTON] = true; - mButtonVisibilities[ALLOW_ONE_TIME_BUTTON] = false; - } else { - mButtonVisibilities[ALLOW_FOREGROUND_BUTTON] = true; - } - } - } - } else { - if (groupState.mGroup.hasPermissionWithBackgroundMode() - || groupState.mGroup.isBackgroundGroup()) { - if (mCouldHaveFgCapabilities) { - // Only allow granting of background location - messageId = groupState.mGroup.getBackgroundRequest(); - detailMessageId = groupState.mGroup.getBackgroundRequestDetail(); - mButtonVisibilities[ALLOW_BUTTON] = false; - mButtonVisibilities[ALLOW_ONE_TIME_BUTTON] = false; - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - } else { - if (needForegroundPermission && needBackgroundPermission) { - // Case: sdk < R, BG/FG permission requesting both - messageId = groupState.mGroup.getBackgroundRequest(); - detailMessageId = groupState.mGroup.getBackgroundRequestDetail(); - mButtonVisibilities[ALLOW_BUTTON] = false; - mButtonVisibilities[ALLOW_FOREGROUND_BUTTON] = true; - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - } else if (needForegroundPermission) { - // Case: sdk < R, BG/FG permission requesting FG only - messageId = groupState.mGroup.getRequest(); - mButtonVisibilities[ALLOW_BUTTON] = false; - mButtonVisibilities[ALLOW_FOREGROUND_BUTTON] = true; - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - } else if (needBackgroundPermission) { - // Case: sdk < R, BG/FG permission requesting BG only - messageId = groupState.mGroup.getUpgradeRequest(); - detailMessageId = groupState.mGroup.getUpgradeRequestDetail(); - mButtonVisibilities[ALLOW_BUTTON] = false; - mButtonVisibilities[DENY_BUTTON] = false; - mButtonVisibilities[ALLOW_ONE_TIME_BUTTON] = false; - if (mAppPermissions.getPermissionGroup( - groupState.mGroup.getName()).isOneTime()) { - mButtonVisibilities[NO_UPGRADE_OT_BUTTON] = - !isBackgroundPermissionUserSet; - mButtonVisibilities[NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON] = - isBackgroundPermissionUserSet; - } else { - mButtonVisibilities[NO_UPGRADE_BUTTON] = - !isBackgroundPermissionUserSet; - mButtonVisibilities[NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON] = - isBackgroundPermissionUserSet; - } - } else { - // Not reached as the permissions should be auto-granted - return false; - } - } - } else { - // Case: sdk < R, Requesting normal permission - messageId = groupState.mGroup.getRequest(); - mButtonVisibilities[DENY_BUTTON] = - !isForegroundPermissionUserSet; - mButtonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = - isForegroundPermissionUserSet; - if (groupState.mGroup.getName().equals(Manifest.permission_group.CAMERA) - || groupState.mGroup.getName().equals( - Manifest.permission_group.MICROPHONE)) { - mButtonVisibilities[ALLOW_BUTTON] = false; - if (mCouldHaveFgCapabilities - || Utils.isEmergencyApp(this, mCallingPackage)) { - mButtonVisibilities[ALLOW_ALWAYS_BUTTON] = true; - mButtonVisibilities[ALLOW_ONE_TIME_BUTTON] = false; - } else { - mButtonVisibilities[ALLOW_FOREGROUND_BUTTON] = true; - } - } - } - } - - CharSequence message = getRequestMessage(appLabel, groupState.mGroup, this, - messageId); - - Spanned detailMessage = null; - if (detailMessageId != 0) { - detailMessage = - new SpannableString(getText(detailMessageId)); - Annotation[] annotations = detailMessage.getSpans( - 0, detailMessage.length(), Annotation.class); - int numAnnotations = annotations.length; - for (int i = 0; i < numAnnotations; i++) { - Annotation annotation = annotations[i]; - if (annotation.getValue().equals(ANNOTATION_ID)) { - int start = detailMessage.getSpanStart(annotation); - int end = detailMessage.getSpanEnd(annotation); - ClickableSpan clickableSpan = getLinkToAppPermissions(groupState); - SpannableString spannableString = - new SpannableString(detailMessage); - spannableString.setSpan(clickableSpan, start, end, 0); - detailMessage = spannableString; - mButtonVisibilities[LINK_TO_SETTINGS] = true; - break; - } - } - } - - // Set the permission message as the title so it can be announced. - setTitle(message); - - mViewHandler.updateUi(groupState.mGroup.getName(), numGrantRequests, currentIndex, - icon, message, detailMessage, mButtonVisibilities); - - return true; - } - - if (groupState.mState != GroupState.STATE_SKIPPED) { - currentIndex++; - } - } - - return false; - } - - private void sendToSettings(GroupState groupState) { - if (mActivityResultCallback == null) { - startAppPermissionFragment(groupState); - mActivityResultCallback = data -> { - if (data == null || data.getStringExtra( - EXTRA_RESULT_PERMISSION_INTERACTED) == null) { - // User didn't interact, count against rate limit - if (groupState.mGroup.isUserSet()) { - groupState.mGroup.setUserFixed(true); - } else { - groupState.mGroup.setUserSet(true); - } - } - mPermissionGroupsToSkip.add(groupState.mGroup.getName()); - }; - } - } - - private ClickableSpan getLinkToAppPermissions(GroupState groupState) { + private ClickableSpan getLinkToAppPermissions(RequestInfo info) { return new ClickableSpan() { @Override public void onClick(View widget) { - logGrantPermissionActivityButtons(groupState.mGroup.getName(), LINKED_TO_SETTINGS); - startAppPermissionFragment(groupState); - mActivityResultCallback = data -> { - if (data != null) { - String groupName = data.getStringExtra(EXTRA_RESULT_PERMISSION_INTERACTED); - if (groupName != null) { - mPermissionGroupsToSkip.add(groupName); - int result = data.getIntExtra(EXTRA_RESULT_PERMISSION_RESULT, -1); - logSettingsInteraction(groupName, result); - } - } - }; + logGrantPermissionActivityButtons(info.getGroupName(), LINKED_TO_SETTINGS); + mViewModel.sendToSettingsFromLink(GrantPermissionsActivity.this, + info.getGroupName()); } }; } - private void logSettingsInteraction(String groupName, int result) { - GroupState foregroundGroupState = getForegroundGroupState(groupName); - GroupState backgroundGroupState = getBackgroundGroupState(groupName); - switch (result) { - case GRANTED_ALWAYS: - if (foregroundGroupState != null) { - reportRequestResult(foregroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS); - } - if (backgroundGroupState != null) { - reportRequestResult(backgroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS); - } - break; - case GRANTED_FOREGROUND_ONLY: - if (foregroundGroupState != null) { - reportRequestResult(foregroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS); - } - if (backgroundGroupState != null) { - reportRequestResult(backgroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS); - } - break; - case DENIED: - if (foregroundGroupState != null) { - reportRequestResult(foregroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS); - } - if (backgroundGroupState != null) { - reportRequestResult(backgroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS); - } - break; - case DENIED_DO_NOT_ASK_AGAIN: - if (foregroundGroupState != null) { - reportRequestResult(foregroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE_IN_SETTINGS); - } - if (backgroundGroupState != null) { - reportRequestResult(backgroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE_IN_SETTINGS); - } - break; - } - } - - private void startAppPermissionFragment(GroupState groupState) { - Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION) - .putExtra(Intent.EXTRA_PACKAGE_NAME, mAppPermissions.getPackageInfo().packageName) - .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupState.mGroup.getName()) - .putExtra(Intent.EXTRA_USER, groupState.mGroup.getUser()) - .putExtra(EXTRA_CALLER_NAME, GrantPermissionsActivity.class.getName()) - .putExtra(Constants.EXTRA_SESSION_ID, mRequestId) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivityForResult(intent, APP_PERMISSION_REQUEST_CODE); - } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == APP_PERMISSION_REQUEST_CODE && mActivityResultCallback != null) { - mActivityResultCallback.accept(data); - mActivityResultCallback = null; + super.onActivityResult(requestCode, resultCode, data); + Consumer<Intent> callback = mViewModel.getActivityResultCallback(); + + if (requestCode == APP_PERMISSION_REQUEST_CODE && callback != null) { + callback.accept(data); + mViewModel.setActivityResultCallback(null); } } @Override public void onPermissionGrantResult(String name, @GrantPermissionsViewHandler.Result int result) { - GroupState foregroundGroupState = getForegroundGroupState(name); - GroupState backgroundGroupState = getBackgroundGroupState(name); - if (result == GRANTED_ALWAYS || result == GRANTED_FOREGROUND_ONLY || result == DENIED_DO_NOT_ASK_AGAIN) { KeyguardManager kgm = getSystemService(KeyguardManager.class); @@ -1005,8 +351,8 @@ public class GrantPermissionsActivity extends Activity kgm.requestDismissKeyguard(this, new KeyguardManager.KeyguardDismissCallback() { @Override public void onDismissError() { - Log.e(LOG_TAG, "Cannot dismiss keyguard perm=" + name + " result=" - + result); + Log.e(LOG_TAG, "Cannot dismiss keyguard perm=" + name + + " result=" + result); } @Override @@ -1027,110 +373,13 @@ public class GrantPermissionsActivity extends Activity } logGrantPermissionActivityButtons(name, result); - switch (result) { - case CANCELED: - if (foregroundGroupState != null) { - reportRequestResult(foregroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED); - } - if (backgroundGroupState != null) { - reportRequestResult(backgroundGroupState.affectedPermissions, - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED); - } - setResultAndFinish(); - return; - case GRANTED_ALWAYS : - if (foregroundGroupState != null) { - onPermissionGrantResultSingleState(foregroundGroupState, true, false, false); - } - if (backgroundGroupState != null) { - onPermissionGrantResultSingleState(backgroundGroupState, true, false, false); - } - break; - case GRANTED_FOREGROUND_ONLY : - if (foregroundGroupState != null) { - onPermissionGrantResultSingleState(foregroundGroupState, true, false, false); - } - if (backgroundGroupState != null) { - onPermissionGrantResultSingleState(backgroundGroupState, false, false, false); - } - break; - case GRANTED_ONE_TIME: - if (foregroundGroupState != null) { - onPermissionGrantResultSingleState(foregroundGroupState, true, true, false); - } - if (backgroundGroupState != null) { - onPermissionGrantResultSingleState(backgroundGroupState, false, true, false); - } - break; - case DENIED : - if (foregroundGroupState != null) { - onPermissionGrantResultSingleState(foregroundGroupState, false, false, false); - } - if (backgroundGroupState != null) { - onPermissionGrantResultSingleState(backgroundGroupState, false, false, false); - } - break; - case DENIED_DO_NOT_ASK_AGAIN : - if (foregroundGroupState != null) { - onPermissionGrantResultSingleState(foregroundGroupState, false, false, true); - } - if (backgroundGroupState != null) { - onPermissionGrantResultSingleState(backgroundGroupState, false, false, true); - } - break; - } - - if (!showNextPermissionGroupGrantRequest()) { + mViewModel.onPermissionGrantResult(name, result); + showNextRequest(); + if (result == CANCELED) { setResultAndFinish(); } } - /** - * Grants or revoked the affected permissions for a single {@link GroupState}. - * - * @param groupState The group state with the permissions to grant/revoke - * @param granted {@code true} if the permissions should be granted, {@code false} if they - * should be revoked - * @param isOneTime if the permission is temporary and should be revoked automatically - * @param doNotAskAgain if the permissions should be revoked should be app be allowed to ask - * again for the same permissions? - */ - private void onPermissionGrantResultSingleState(GroupState groupState, boolean granted, - boolean isOneTime, boolean doNotAskAgain) { - if (groupState != null && groupState.mGroup != null - && groupState.mState == GroupState.STATE_UNKNOWN) { - if (granted) { - int permissionGrantRequestResult = - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED; - - if (isOneTime) { - groupState.mGroup.setOneTime(true); - permissionGrantRequestResult = - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_ONE_TIME; - } else { - groupState.mGroup.setOneTime(false); - } - - groupState.mGroup.grantRuntimePermissions(true, doNotAskAgain, - groupState.affectedPermissions); - groupState.mState = GroupState.STATE_ALLOWED; - - reportRequestResult(groupState.affectedPermissions, permissionGrantRequestResult); - } else { - groupState.mGroup.revokeRuntimePermissions(doNotAskAgain, - groupState.affectedPermissions); - groupState.mGroup.setOneTime(false); - groupState.mState = GroupState.STATE_DENIED; - - reportRequestResult(groupState.affectedPermissions, doNotAskAgain - ? - PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE - : PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED); - } - } - } - @Override public void onBackPressed() { mViewHandler.onBackPressed(); @@ -1139,37 +388,30 @@ public class GrantPermissionsActivity extends Activity @Override public void finish() { setResultIfNeeded(RESULT_CANCELED); - if (mAutoGrantPermissionsNotifier != null) { - mAutoGrantPermissionsNotifier.notifyOfAutoGrantPermissions(true); - } + mViewModel.autoGrantNotify(); super.finish(); } - private PackageInfo getCallingPackageInfo() { - try { - return getPackageManager().getPackageInfo(mCallingPackage, - PackageManager.GET_PERMISSIONS); - } catch (NameNotFoundException e) { - Log.i(LOG_TAG, "No package: " + mCallingPackage, e); - return null; - } - } - private void setResultIfNeeded(int resultCode) { if (!mResultSet) { mResultSet = true; - logRequestedPermissionGroups(); + mViewModel.logRequestedPermissionGroups(); Intent result = new Intent(PackageManager.ACTION_REQUEST_PERMISSIONS); result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES, mRequestedPermissions); - PackageManager pm = getPackageManager(); - int numRequestedPermissions = mRequestedPermissions.length; - int[] grantResults = new int[numRequestedPermissions]; - for (int i = 0; i < numRequestedPermissions; i++) { - grantResults[i] = pm.checkPermission(mRequestedPermissions[i], mCallingPackage); - } + if (mViewModel.getRequestInfosLiveData().isInitialized() + && mViewModel.shouldReturnPermissionState()) { + PackageManager pm = getPackageManager(); + int numRequestedPermissions = mRequestedPermissions.length; + int[] grantResults = new int[numRequestedPermissions]; + for (int i = 0; i < numRequestedPermissions; i++) { + grantResults[i] = pm.checkPermission(mRequestedPermissions[i], mCallingPackage); + } - result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS, grantResults); + result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS, grantResults); + } else { + result.putExtra(PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS, new int[0]); + } setResult(resultCode, result); } } @@ -1179,79 +421,6 @@ public class GrantPermissionsActivity extends Activity finish(); } - private void logRequestedPermissionGroups() { - if (mRequestGrantPermissionGroups.isEmpty()) { - return; - } - - final int groupCount = mRequestGrantPermissionGroups.size(); - List<AppPermissionGroup> groups = new ArrayList<>(groupCount); - for (GroupState groupState : mRequestGrantPermissionGroups.values()) { - groups.add(groupState.mGroup); - } - - SafetyNetLogger.logPermissionsRequested(mAppPermissions.getPackageInfo(), groups); - } - - /** - * Get the actually requested permissions when a permission is requested. - * - * <p>>In some cases requesting to grant a single permission requires the system to grant - * additional permissions. E.g. before N-MR1 a single permission of a group caused the whole - * group to be granted. Another case are permissions that are split into two. For apps that - * target an SDK before the split, this method automatically adds the split off permission. - * - * @param permission The requested permission - * - * @return The actually requested permissions - */ - private ArrayList<String> computeAffectedPermissions(String permission) { - int requestingAppTargetSDK = - mAppPermissions.getPackageInfo().applicationInfo.targetSdkVersion; - - // If a permission is split, all permissions the original permission is split into are - // affected - ArrayList<String> extendedBySplitPerms = new ArrayList<>(); - extendedBySplitPerms.add(permission); - - List<PermissionManager.SplitPermissionInfo> splitPerms = getSystemService( - PermissionManager.class).getSplitPermissions(); - int numSplitPerms = splitPerms.size(); - for (int i = 0; i < numSplitPerms; i++) { - PermissionManager.SplitPermissionInfo splitPerm = splitPerms.get(i); - - if (requestingAppTargetSDK < splitPerm.getTargetSdk() - && permission.equals(splitPerm.getSplitPermission())) { - extendedBySplitPerms.addAll(splitPerm.getNewPermissions()); - } - } - - // For <= N_MR1 apps all permissions of the groups of the requested permissions are affected - if (requestingAppTargetSDK <= Build.VERSION_CODES.N_MR1) { - ArrayList<String> extendedBySplitPermsAndGroup = new ArrayList<>(); - - int numExtendedBySplitPerms = extendedBySplitPerms.size(); - for (int splitPermNum = 0; splitPermNum < numExtendedBySplitPerms; splitPermNum++) { - AppPermissionGroup group = mAppPermissions.getGroupForPermission( - extendedBySplitPerms.get(splitPermNum)); - - if (group == null) { - continue; - } - - ArrayList<Permission> permissionsInGroup = group.getPermissions(); - int numPermissionsInGroup = permissionsInGroup.size(); - for (int permNum = 0; permNum < numPermissionsInGroup; permNum++) { - extendedBySplitPermsAndGroup.add(permissionsInGroup.get(permNum).getName()); - } - } - - return extendedBySplitPermsAndGroup; - } else { - return extendedBySplitPerms; - } - } - private void logGrantPermissionActivityButtons(String permissionGroupName, int grantResult) { int clickedButton = 0; int presentedButtons = getButtonState(); @@ -1295,13 +464,7 @@ public class GrantPermissionsActivity extends Activity break; } - PermissionControllerStatsLog.write(GRANT_PERMISSIONS_ACTIVITY_BUTTON_ACTIONS, - permissionGroupName, mCallingUid, mCallingPackage, presentedButtons, - clickedButton, mRequestId); - Log.v(LOG_TAG, "Logged buttons presented and clicked permissionGroupName=" - + permissionGroupName + " uid=" + mCallingUid + " package=" + mCallingPackage - + " presentedButtons=" + presentedButtons + " clickedButton=" + clickedButton - + " sessionId=" + mRequestId); + mViewModel.logClickedButtons(permissionGroupName, clickedButton, presentedButtons); } private int getButtonState() { @@ -1317,51 +480,4 @@ public class GrantPermissionsActivity extends Activity } return buttonState; } - - private static final class GroupState { - static final int STATE_UNKNOWN = 0; - static final int STATE_ALLOWED = 1; - static final int STATE_DENIED = 2; - static final int STATE_SKIPPED = 3; - - final AppPermissionGroup mGroup; - int mState = STATE_UNKNOWN; - - /** Permissions of this group that need to be granted, null == no permissions of group */ - String[] affectedPermissions; - - GroupState(AppPermissionGroup group) { - mGroup = group; - } - } - - private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener { - final int mCallingPackageUid; - - PermissionChangeListener() throws NameNotFoundException { - mCallingPackageUid = getPackageManager().getPackageUid(mCallingPackage, 0); - } - - @Override - public void onPermissionsChanged(int uid) { - if (uid == mCallingPackageUid) { - updateIfPermissionsWereGranted(); - } - } - } - - /** - * Creates the AutoGrantPermissionsNotifier lazily in case there's no policy set - * device-wide (common case). - * - * @return An initalized {@code AutoGrantPermissionsNotifier} instance. - */ - private @NonNull AutoGrantPermissionsNotifier getAutoGrantNotifier() { - if (mAutoGrantPermissionsNotifier == null) { - mAutoGrantPermissionsNotifier = new AutoGrantPermissionsNotifier( - this, mCallingPackageInfo); - } - - return mAutoGrantPermissionsNotifier; - } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java index 6a7002d3b..7a0a843a6 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/AppPermissionFragment.java @@ -431,6 +431,10 @@ public class AppPermissionFragment extends SettingsWithLargeHeader } private void setResult(@GrantPermissionsViewHandler.Result int result) { + if (!mPackageName.equals( + getActivity().getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME))) { + return; + } Intent intent = new Intent() .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED, mPermGroupName) .putExtra(EXTRA_RESULT_PERMISSION_RESULT, result); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java deleted file mode 100644 index 04a5f439a..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.permissioncontroller.permission.ui.handheld; - -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ALWAYS_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_FOREGROUND_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ONE_TIME_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_AND_DONT_ASK_AGAIN_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON; -import static com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_BUTTON; - -import android.app.Activity; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.os.UserHandle; -import android.text.method.LinkMovementMethod; -import android.transition.ChangeBounds; -import android.transition.TransitionManager; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.WindowManager.LayoutParams; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.AnimationUtils; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.android.permissioncontroller.R; -import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity; -import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler; - -public class GrantPermissionsViewHandlerImpl implements GrantPermissionsViewHandler, - OnClickListener { - - public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME"; - public static final String ARG_GROUP_COUNT = "ARG_GROUP_COUNT"; - public static final String ARG_GROUP_INDEX = "ARG_GROUP_INDEX"; - public static final String ARG_GROUP_ICON = "ARG_GROUP_ICON"; - public static final String ARG_GROUP_MESSAGE = "ARG_GROUP_MESSAGE"; - private static final String ARG_GROUP_DETAIL_MESSAGE = "ARG_GROUP_DETAIL_MESSAGE"; - private static final String ARG_DIALOG_BUTTON_VISIBILITIES = "ARG_DIALOG_BUTTON_VISIBILITIES"; - - // Animation parameters. - private static final long SWITCH_TIME_MILLIS = 75; - private static final long ANIMATION_DURATION_MILLIS = 200; - - private static final SparseArray<Integer> BUTTON_RES_ID_TO_NUM = new SparseArray<>(); - static { - BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_button, ALLOW_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_always_button, - ALLOW_ALWAYS_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_foreground_only_button, - ALLOW_FOREGROUND_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_deny_button, DENY_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_deny_and_dont_ask_again_button, - DENY_AND_DONT_ASK_AGAIN_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_one_time_button, ALLOW_ONE_TIME_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_button, NO_UPGRADE_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_and_dont_ask_again_button, - NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_one_time_button, NO_UPGRADE_OT_BUTTON); - BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_one_time_and_dont_ask_again_button, - NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON); - } - - private final Activity mActivity; - private final String mAppPackageName; - private final UserHandle mUserHandle; - - private ResultListener mResultListener; - - // Configuration of the current dialog - private String mGroupName; - private int mGroupCount; - private int mGroupIndex; - private Icon mGroupIcon; - private CharSequence mGroupMessage; - private CharSequence mDetailMessage; - private boolean[] mButtonVisibilities; - - // Views - private ImageView mIconView; - private TextView mMessageView; - private TextView mDetailMessageView; - private Button[] mButtons; - private ViewGroup mRootView; - - public GrantPermissionsViewHandlerImpl(Activity activity, String appPackageName, - @NonNull UserHandle userHandle) { - mActivity = activity; - mAppPackageName = appPackageName; - mUserHandle = userHandle; - } - - @Override - public GrantPermissionsViewHandlerImpl setResultListener(ResultListener listener) { - mResultListener = listener; - return this; - } - - @Override - public void saveInstanceState(Bundle arguments) { - arguments.putString(ARG_GROUP_NAME, mGroupName); - arguments.putInt(ARG_GROUP_COUNT, mGroupCount); - arguments.putInt(ARG_GROUP_INDEX, mGroupIndex); - arguments.putParcelable(ARG_GROUP_ICON, mGroupIcon); - arguments.putCharSequence(ARG_GROUP_MESSAGE, mGroupMessage); - arguments.putCharSequence(ARG_GROUP_DETAIL_MESSAGE, mDetailMessage); - arguments.putBooleanArray(ARG_DIALOG_BUTTON_VISIBILITIES, mButtonVisibilities); - } - - @Override - public void loadInstanceState(Bundle savedInstanceState) { - mGroupName = savedInstanceState.getString(ARG_GROUP_NAME); - mGroupMessage = savedInstanceState.getCharSequence(ARG_GROUP_MESSAGE); - mGroupIcon = savedInstanceState.getParcelable(ARG_GROUP_ICON); - mGroupCount = savedInstanceState.getInt(ARG_GROUP_COUNT); - mGroupIndex = savedInstanceState.getInt(ARG_GROUP_INDEX); - mDetailMessage = savedInstanceState.getCharSequence(ARG_GROUP_DETAIL_MESSAGE); - mButtonVisibilities = savedInstanceState.getBooleanArray(ARG_DIALOG_BUTTON_VISIBILITIES); - - updateAll(); - } - - @Override - public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon, - CharSequence message, CharSequence detailMessage, boolean[] buttonVisibilities) { - boolean isNewGroup = mGroupIndex != groupIndex; - - mGroupName = groupName; - mGroupCount = groupCount; - mGroupIndex = groupIndex; - mGroupIcon = icon; - mGroupMessage = message; - mDetailMessage = detailMessage; - mButtonVisibilities = buttonVisibilities; - - // If this is a second (or later) permission and the views exist, then animate. - if (mIconView != null) { - updateAll(); - } - } - - private void updateAll() { - updateDescription(); - updateDetailDescription(); - updateButtons(); - -// Animate change in size -// Grow or shrink the content container to size of new content - ChangeBounds growShrinkToNewContentSize = new ChangeBounds(); - growShrinkToNewContentSize.setDuration(ANIMATION_DURATION_MILLIS); - growShrinkToNewContentSize.setInterpolator(AnimationUtils.loadInterpolator(mActivity, - android.R.interpolator.fast_out_slow_in)); - TransitionManager.beginDelayedTransition(mRootView, growShrinkToNewContentSize); - } - - @Override - public View createView() { - mRootView = (ViewGroup) LayoutInflater.from(mActivity) - .inflate(R.layout.grant_permissions, null); - - int h = mActivity.getResources().getDisplayMetrics().heightPixels; - mRootView.setMinimumHeight(h); - mRootView.findViewById(R.id.grant_singleton).setOnClickListener(this); // Cancel dialog - mRootView.findViewById(R.id.grant_dialog).setOnClickListener(this); // Swallow click event - - mMessageView = mRootView.findViewById(R.id.permission_message); - mDetailMessageView = mRootView.findViewById(R.id.detail_message); - mDetailMessageView.setMovementMethod(LinkMovementMethod.getInstance()); - mIconView = mRootView.findViewById(R.id.permission_icon); - - mButtons = new Button[GrantPermissionsActivity.NEXT_BUTTON]; - - int numButtons = BUTTON_RES_ID_TO_NUM.size(); - for (int i = 0; i < numButtons; i++) { - Button button = mRootView.findViewById(BUTTON_RES_ID_TO_NUM.keyAt(i)); - button.setOnClickListener(this); - mButtons[BUTTON_RES_ID_TO_NUM.valueAt(i)] = button; - } - - if (mGroupName != null) { - updateAll(); - } - - return mRootView; - } - - @Override - public void updateWindowAttributes(LayoutParams outLayoutParams) { - // No-op - } - - private void updateDescription() { - if (mGroupIcon != null) { - mIconView.setImageDrawable(mGroupIcon.loadDrawable(mActivity)); - } - mMessageView.setText(mGroupMessage); - } - - private void updateDetailDescription() { - if (mDetailMessage == null) { - mDetailMessageView.setVisibility(View.GONE); - } else { - mDetailMessageView.setText(mDetailMessage); - mDetailMessageView.setVisibility(View.VISIBLE); - } - } - - private void updateButtons() { - int numButtons = BUTTON_RES_ID_TO_NUM.size(); - for (int i = 0; i < numButtons; i++) { - int pos = BUTTON_RES_ID_TO_NUM.valueAt(i); - mButtons[pos].setVisibility(mButtonVisibilities[pos] ? View.VISIBLE : View.GONE); - } - } - - @Override - public void onClick(View view) { - int id = view.getId(); - if (id == R.id.grant_singleton) { - if (mResultListener != null) { - mResultListener.onPermissionGrantResult(mGroupName, CANCELED); - } else { - mActivity.finish(); - } - return; - } - int button = -1; - try { - button = BUTTON_RES_ID_TO_NUM.get(id); - } catch (NullPointerException e) { - // Clicked a view which is not one of the defined buttons - return; - } - switch (button) { - case ALLOW_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, GRANTED_ALWAYS); - } - break; - case ALLOW_FOREGROUND_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, - GRANTED_FOREGROUND_ONLY); - } - break; - case ALLOW_ALWAYS_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, - GRANTED_ALWAYS); - } - break; - case ALLOW_ONE_TIME_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, GRANTED_ONE_TIME); - } - break; - case DENY_BUTTON: - case NO_UPGRADE_BUTTON: - case NO_UPGRADE_OT_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, DENIED); - } - break; - case DENY_AND_DONT_ASK_AGAIN_BUTTON: - case NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON: - case NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON: - if (mResultListener != null) { - view.performAccessibilityAction( - AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); - mResultListener.onPermissionGrantResult(mGroupName, - DENIED_DO_NOT_ASK_AGAIN); - } - break; - } - - } - - @Override - public void onBackPressed() { - if (mResultListener != null) { - mResultListener.onPermissionGrantResult(mGroupName, CANCELED); - } else { - mActivity.finish(); - } - } - -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt new file mode 100644 index 000000000..4d5091403 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/GrantPermissionsViewHandlerImpl.kt @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.ui.handheld + +import android.app.Activity +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.UserHandle +import android.text.method.LinkMovementMethod +import android.transition.ChangeBounds +import android.transition.TransitionManager +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import android.view.WindowManager.LayoutParams +import android.view.accessibility.AccessibilityNodeInfo +import android.view.animation.AnimationUtils +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView + +import com.android.permissioncontroller.R +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ALWAYS_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_FOREGROUND_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ONE_TIME_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NEXT_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.CANCELED +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ALWAYS +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ONE_TIME + +class GrantPermissionsViewHandlerImpl( + private val mActivity: Activity, + private val mAppPackageName: String, + private val mUserHandle: UserHandle +) : GrantPermissionsViewHandler, OnClickListener { + + private var resultListener: GrantPermissionsViewHandler.ResultListener? = null + + // Configuration of the current dialog + private var groupName: String? = null + private var groupCount: Int = 0 + private var groupIndex: Int = 0 + private var groupIcon: Icon? = null + private var groupMessage: CharSequence? = null + private var detailMessage: CharSequence? = null + private var buttonVisibilities: BooleanArray? = null + + // Views + private var iconView: ImageView? = null + private var messageView: TextView? = null + private var detailMessageView: TextView? = null + private var buttons: Array<Button?>? = null + private var rootView: ViewGroup? = null + + override fun setResultListener( + listener: GrantPermissionsViewHandler.ResultListener + ): GrantPermissionsViewHandlerImpl { + resultListener = listener + return this + } + + override fun saveInstanceState(arguments: Bundle) { + arguments.putString(ARG_GROUP_NAME, groupName) + arguments.putInt(ARG_GROUP_COUNT, groupCount) + arguments.putInt(ARG_GROUP_INDEX, groupIndex) + arguments.putParcelable(ARG_GROUP_ICON, groupIcon) + arguments.putCharSequence(ARG_GROUP_MESSAGE, groupMessage) + arguments.putCharSequence(ARG_GROUP_DETAIL_MESSAGE, detailMessage) + arguments.putBooleanArray(ARG_DIALOG_BUTTON_VISIBILITIES, buttonVisibilities) + } + + override fun loadInstanceState(savedInstanceState: Bundle) { + groupName = savedInstanceState.getString(ARG_GROUP_NAME) + groupMessage = savedInstanceState.getCharSequence(ARG_GROUP_MESSAGE) + groupIcon = savedInstanceState.getParcelable(ARG_GROUP_ICON) + groupCount = savedInstanceState.getInt(ARG_GROUP_COUNT) + groupIndex = savedInstanceState.getInt(ARG_GROUP_INDEX) + detailMessage = savedInstanceState.getCharSequence(ARG_GROUP_DETAIL_MESSAGE) + buttonVisibilities = savedInstanceState.getBooleanArray(ARG_DIALOG_BUTTON_VISIBILITIES) + + updateAll() + } + + override fun updateUi( + groupName: String, + groupCount: Int, + groupIndex: Int, + icon: Icon, + message: CharSequence, + detailMessage: CharSequence?, + buttonVisibilities: BooleanArray + ) { + + this.groupName = groupName + this.groupCount = groupCount + this.groupIndex = groupIndex + groupIcon = icon + groupMessage = message + this.detailMessage = detailMessage + this.buttonVisibilities = buttonVisibilities + + // If this is a second (or later) permission and the views exist, then animate. + if (iconView != null) { + updateAll() + } + } + + private fun updateAll() { + updateDescription() + updateDetailDescription() + updateButtons() + + // Animate change in size + // Grow or shrink the content container to size of new content + val growShrinkToNewContentSize = ChangeBounds() + growShrinkToNewContentSize.duration = ANIMATION_DURATION_MILLIS + growShrinkToNewContentSize.interpolator = AnimationUtils.loadInterpolator(mActivity, + android.R.interpolator.fast_out_slow_in) + TransitionManager.beginDelayedTransition(rootView, growShrinkToNewContentSize) + } + + override fun createView(): View { + val rootView = LayoutInflater.from(mActivity) + .inflate(R.layout.grant_permissions, null) as ViewGroup + this.rootView = rootView + + val h = mActivity.resources.displayMetrics.heightPixels + rootView.minimumHeight = h + // Cancel dialog + rootView.findViewById<View>(R.id.grant_singleton)!!.setOnClickListener(this) + // Swallow click event + rootView.findViewById<View>(R.id.grant_dialog)!!.setOnClickListener(this) + + messageView = rootView.findViewById(R.id.permission_message) + detailMessageView = rootView.findViewById(R.id.detail_message) + detailMessageView!!.movementMethod = LinkMovementMethod.getInstance() + iconView = rootView.findViewById(R.id.permission_icon) + + val buttons = arrayOfNulls<Button>(NEXT_BUTTON) + + val numButtons = BUTTON_RES_ID_TO_NUM.size() + for (i in 0 until numButtons) { + val button = rootView.findViewById<Button>(BUTTON_RES_ID_TO_NUM.keyAt(i)) + button!!.setOnClickListener(this) + buttons[BUTTON_RES_ID_TO_NUM.valueAt(i)] = button + } + + this.buttons = buttons + if (groupName != null) { + updateAll() + } + + return rootView + } + + override fun updateWindowAttributes(outLayoutParams: LayoutParams) { + // No-op + } + + private fun updateDescription() { + if (groupIcon != null) { + iconView!!.setImageDrawable(groupIcon!!.loadDrawable(mActivity)) + } + messageView!!.text = groupMessage + } + + private fun updateDetailDescription() { + if (detailMessage == null) { + detailMessageView!!.visibility = View.GONE + } else { + detailMessageView!!.text = detailMessage + detailMessageView!!.visibility = View.VISIBLE + } + } + + private fun updateButtons() { + val numButtons = BUTTON_RES_ID_TO_NUM.size() + for (i in 0 until numButtons) { + val pos = BUTTON_RES_ID_TO_NUM.valueAt(i) + buttons!![pos]!!.visibility = if (buttonVisibilities!![pos]) { + View.VISIBLE + } else { + View.GONE + } + } + } + + override fun onClick(view: View) { + val id = view.id + if (id == R.id.grant_singleton) { + if (resultListener != null) { + resultListener!!.onPermissionGrantResult(groupName, CANCELED) + } else { + mActivity.finish() + } + return + } + var button = -1 + try { + button = BUTTON_RES_ID_TO_NUM.get(id) + } catch (e: NullPointerException) { + // Clicked a view which is not one of the defined buttons + return + } + + when (button) { + ALLOW_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, GRANTED_ALWAYS) + } + ALLOW_FOREGROUND_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, GRANTED_FOREGROUND_ONLY) + } + ALLOW_ALWAYS_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, GRANTED_ALWAYS) + } + ALLOW_ONE_TIME_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, GRANTED_ONE_TIME) + } + DENY_BUTTON, NO_UPGRADE_BUTTON, NO_UPGRADE_OT_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, DENIED) + } + DENY_AND_DONT_ASK_AGAIN_BUTTON, NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON, + NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON -> if (resultListener != null) { + view.performAccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null) + resultListener!!.onPermissionGrantResult(groupName, DENIED_DO_NOT_ASK_AGAIN) + } + } + } + + override fun onBackPressed() { + if (resultListener == null) { + mActivity.finish() + return + } + resultListener?.onPermissionGrantResult(groupName, CANCELED) + } + + companion object { + + val ARG_GROUP_NAME = "ARG_GROUP_NAME" + val ARG_GROUP_COUNT = "ARG_GROUP_COUNT" + val ARG_GROUP_INDEX = "ARG_GROUP_INDEX" + val ARG_GROUP_ICON = "ARG_GROUP_ICON" + val ARG_GROUP_MESSAGE = "ARG_GROUP_MESSAGE" + private val ARG_GROUP_DETAIL_MESSAGE = "ARG_GROUP_DETAIL_MESSAGE" + private val ARG_DIALOG_BUTTON_VISIBILITIES = "ARG_DIALOG_BUTTON_VISIBILITIES" + + // Animation parameters. + private val SWITCH_TIME_MILLIS: Long = 75 + private val ANIMATION_DURATION_MILLIS: Long = 200 + + private val BUTTON_RES_ID_TO_NUM = SparseIntArray() + + init { + BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_button, ALLOW_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_always_button, + ALLOW_ALWAYS_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_foreground_only_button, + ALLOW_FOREGROUND_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_deny_button, DENY_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_deny_and_dont_ask_again_button, + DENY_AND_DONT_ASK_AGAIN_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_allow_one_time_button, + ALLOW_ONE_TIME_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_button, + NO_UPGRADE_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_and_dont_ask_again_button, + NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_one_time_button, + NO_UPGRADE_OT_BUTTON) + BUTTON_RES_ID_TO_NUM.put(R.id.permission_no_upgrade_one_time_and_dont_ask_again_button, + NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON) + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.java index 1b8d8a098..d75e61133 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/handheld/PermissionPreference.java @@ -575,7 +575,8 @@ class PermissionPreference extends MultiTargetSwitchPreference { Bundle args = new Bundle(); args.putCharSequence(BackgroundAccessChooser.TITLE, - getRequestMessage(getAppLabel(), mGroup, getContext(), mGroup.getRequest())); + getRequestMessage(getAppLabel(), mGroup.getApp().packageName, mGroup.getName(), + getContext(), mGroup.getRequest())); args.putString(BackgroundAccessChooser.KEY, getKey()); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt new file mode 100644 index 000000000..238a92999 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -0,0 +1,989 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.permissioncontroller.permission.ui.model + +import android.app.Activity +import android.app.Application +import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED +import android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED +import android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET +import android.os.Build +import android.os.Bundle +import android.os.Process +import android.permission.PermissionManager +import android.util.Log +import androidx.core.util.Consumer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.permissioncontroller.Constants +import com.android.permissioncontroller.PermissionControllerStatsLog +import com.android.permissioncontroller.PermissionControllerStatsLog.GRANT_PERMISSIONS_ACTIVITY_BUTTON_ACTIONS +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_DENIED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_POLICY_FIXED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_RESTRICTED_PERMISSION +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_USER_FIXED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE_IN_SETTINGS +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_ONE_TIME +import com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED +import com.android.permissioncontroller.permission.data.LightAppPermGroupLiveData +import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData +import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData +import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData +import com.android.permissioncontroller.permission.data.get +import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup +import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo +import com.android.permissioncontroller.permission.model.livedatatypes.LightPermGroupInfo +import com.android.permissioncontroller.permission.ui.AutoGrantPermissionsNotifier +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_FOREGROUND_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.ALLOW_ONE_TIME_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.DENY_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.LINK_TO_SETTINGS +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NEXT_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsActivity.NO_UPGRADE_OT_BUTTON +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ALWAYS +import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED +import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT +import com.android.permissioncontroller.permission.utils.KotlinUtils +import com.android.permissioncontroller.permission.utils.SafetyNetLogger +import com.android.permissioncontroller.permission.utils.Utils +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +/** + * ViewModel for the GrantPermissionsActivity. Tracks all permission groups that are affected by + * the permissions requested by the user, and generates a RequestInfo object for each group, if + * action is needed. It will not return any data if one of the requests is malformed. + * + * @param app: The current application + * @param packageName: The packageName permissions are being requested for + * @param requestedPermissions: The list of permissions requested + * @param sessionId: A long to identify this session + * @param storedState: Previous state, if this activity was stopped and is being recreated + */ +class GrantPermissionsViewModel( + private val app: Application, + private val packageName: String, + private val requestedPermissions: List<String>, + private val sessionId: Long, + private val storedState: Bundle? +) : ViewModel() { + private val LOG_TAG = GrantPermissionsViewModel::class.java.simpleName + private val user = Process.myUserHandle() + private val packageInfoLiveData = LightPackageInfoLiveData[packageName, user] + private val permissionPolicy = app.getSystemService(DevicePolicyManager::class.java)!! + .getPermissionPolicy(null) + private val permGroupsToSkip = mutableListOf<String>() + private var groupStates = mutableMapOf<Pair<String, Boolean>, GroupState>() + + private var autoGrantNotifier: AutoGrantPermissionsNotifier? = null + private fun getAutoGrantNotifier(): AutoGrantPermissionsNotifier { + autoGrantNotifier = AutoGrantPermissionsNotifier(app, packageInfo.toPackageInfo(app)!!) + return autoGrantNotifier!! + } + + private lateinit var packageInfo: LightPackageInfo + + // All permissions that could possibly be affected by the provided requested permissions, before + // filtering system fixed, auto grant, etc. + private var unfilteredAffectedPermissions = requestedPermissions + + /** + * A class which represents a correctly requested permission group, and the buttons and messages + * which should be shown with it. + */ + data class RequestInfo( + val groupInfo: LightPermGroupInfo, + val buttonVisibilities: List<Boolean> = List(NEXT_BUTTON) { false }, + val message: RequestMessage = RequestMessage.FG_MESSAGE, + val detailMessage: RequestMessage = RequestMessage.NO_MESSAGE, + val sendToSettingsImmediately: Boolean = false + ) { + val groupName = groupInfo.name + } + + var activityResultCallback: Consumer<Intent>? = null + + /** + * A LiveData which holds a list of the currently pending RequestInfos + */ + val requestInfosLiveData = object : + SmartUpdateMediatorLiveData<List<RequestInfo>>() { + private val LOG_TAG = GrantPermissionsViewModel::class.java.simpleName + private val packagePermissionsLiveData = PackagePermissionsLiveData[packageName, user] + private val appPermGroupLiveDatas = mutableMapOf<String, LightAppPermGroupLiveData>() + + init { + GlobalScope.launch(Main.immediate) { + val groups = packagePermissionsLiveData.getInitializedValue() + if (groups == null || groups.isEmpty()) { + Log.w(LOG_TAG, "Package $packageName not found") + value = null + return@launch + } + packageInfo = packageInfoLiveData.getInitializedValue() + + if (packageInfo.requestedPermissions.isEmpty() || + packageInfo.targetSdkVersion < Build.VERSION_CODES.M) { + Log.w(LOG_TAG, "Package $packageName has no requested permissions, or " + + "is a pre-M app") + value = null + return@launch + } + + val allAffectedPermissions = requestedPermissions.toMutableSet() + for (requestedPerm in requestedPermissions) { + allAffectedPermissions.addAll(computeAffectedPermissions(requestedPerm, groups)) + } + unfilteredAffectedPermissions = allAffectedPermissions.toList() + + getAppPermGroups(groups.toMutableMap().apply { + remove(PackagePermissionsLiveData.NON_RUNTIME_NORMAL_PERMS) + }) + } + } + + private fun getAppPermGroups(groups: Map<String, List<String>>) { + + val requestedGroups = groups.filter { (_, perms) -> + perms.any { it in unfilteredAffectedPermissions } + } + + if (requestedGroups.isEmpty()) { + Log.e(LOG_TAG, "None of " + + "$unfilteredAffectedPermissions in $groups") + value = null + return + } + + val getLiveDataFun = { groupName: String -> + LightAppPermGroupLiveData[packageName, groupName, user] + } + setSourcesToDifference(requestedGroups.keys, appPermGroupLiveDatas, getLiveDataFun) + } + + override fun onUpdate() { + if (appPermGroupLiveDatas.any { it.value.isStale }) { + return + } + var newGroups = false + for ((groupName, groupLiveData) in appPermGroupLiveDatas) { + val appPermGroup = groupLiveData.value + if (appPermGroup == null || groupName in permGroupsToSkip) { + if (appPermGroup == null) { + Log.w(LOG_TAG, "Group $packageName $groupName invalid") + } + groupStates[groupName to true]?.state = STATE_SKIPPED + groupStates[groupName to false]?.state = STATE_SKIPPED + continue + } + + packageInfo = appPermGroup.packageInfo + + val states = groupStates.filter { it.key.first == groupName } + if (states.isNotEmpty()) { + // some requests might have been granted, check for that + for ((key, state) in states) { + val allAffectedGranted = state.affectedPermissions.all { perm -> + appPermGroup.permissions[perm]?.isGrantedIncludingAppOp == true + } + if (allAffectedGranted) { + groupStates[key]!!.state = STATE_ALLOWED + } + } + } else { + newGroups = true + } + } + + if (newGroups) { + groupStates = getRequiredGroupStates( + appPermGroupLiveDatas.mapNotNull { it.value.value }) + } + getRequestInfosFromGroupStates() + } + + private fun getRequestInfosFromGroupStates() { + val requestInfos = mutableListOf<RequestInfo>() + for ((key, groupState) in groupStates) { + val groupInfo = groupState.group.permGroupInfo + val (groupName, isBackground) = key + if (groupState.state != STATE_UNKNOWN) { + continue + } + + val fgState = groupStates[groupName to false] + val bgState = groupStates[groupName to true] + var needFgPermissions = false + var needBgPermissions = false + var isFgUserSet = false + var isBgUserSet = false + + if (fgState?.group?.foreground?.isGranted == false) { + needFgPermissions = true + isFgUserSet = fgState.group.foreground.isUserSet + } + + if (bgState?.group?.background?.isGranted == false) { + needBgPermissions = true + isBgUserSet = bgState.group.background.isUserSet + } + + val buttonVisibilities = MutableList(NEXT_BUTTON) { false } + buttonVisibilities[ALLOW_BUTTON] = true + buttonVisibilities[DENY_BUTTON] = true + buttonVisibilities[ALLOW_ONE_TIME_BUTTON] = + Utils.supportsOneTimeGrant(groupName) + var message = RequestMessage.FG_MESSAGE + // Whether or not to use the foreground, background, or no detail message. + // null == + var detailMessage = RequestMessage.NO_MESSAGE + + if (groupState.group.packageInfo.targetSdkVersion >= Build.VERSION_CODES.R) { + if (isBackground || groupState.group.hasPermWithBackgroundMode) { + if (needFgPermissions) { + if (needBgPermissions) { + // Shouldn't be reached as background must be requested as a + // singleton + Log.e(LOG_TAG, "For R+ apps, background permissions must be " + + "requested after foreground permissions are already granted") + value = null + return + } + buttonVisibilities[ALLOW_BUTTON] = false + buttonVisibilities[ALLOW_FOREGROUND_BUTTON] = true + buttonVisibilities[DENY_BUTTON] = !isFgUserSet + buttonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = isFgUserSet + } else if (needBgPermissions) { + // Case: sdk >= R, BG/FG permission requesting BG only + requestInfos.add(RequestInfo( + groupInfo, sendToSettingsImmediately = true)) + continue + } else { + // Not reached as the permissions should be auto-granted + value = null + return + } + } else { + // Case: sdk >= R, Requesting normal permission + buttonVisibilities[DENY_BUTTON] = !isFgUserSet + buttonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = isFgUserSet + } + } else { + if (isBackground || groupState.group.hasPermWithBackgroundMode) { + if (needFgPermissions) { + // Case: sdk < R, BG/FG permission requesting both or FG only + buttonVisibilities[ALLOW_BUTTON] = false + buttonVisibilities[ALLOW_FOREGROUND_BUTTON] = true + buttonVisibilities[DENY_BUTTON] = !isFgUserSet + buttonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = isFgUserSet + if (needBgPermissions) { + // Case: sdk < R, BG/FG permission requesting both + message = RequestMessage.BG_MESSAGE + detailMessage = RequestMessage.BG_MESSAGE + } + } else if (needBgPermissions) { + // Case: sdk < R, BG/FG permission requesting BG only + message = RequestMessage.UPGRADE_MESSAGE + detailMessage = RequestMessage.UPGRADE_MESSAGE + buttonVisibilities[ALLOW_BUTTON] = false + buttonVisibilities[DENY_BUTTON] = false + buttonVisibilities[ALLOW_ONE_TIME_BUTTON] = false + if (groupState.group.isOneTime) { + buttonVisibilities[NO_UPGRADE_OT_BUTTON] = !isBgUserSet + buttonVisibilities[NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON] = + isBgUserSet + } else { + buttonVisibilities[NO_UPGRADE_BUTTON] = !isBgUserSet + buttonVisibilities[NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON] = + isBgUserSet + } + } else { + // Not reached as the permissions should be auto-granted + value = null + return + } + } else { + // Case: sdk < R, Requesting normal permission + buttonVisibilities[DENY_BUTTON] = !isFgUserSet + buttonVisibilities[DENY_AND_DONT_ASK_AGAIN_BUTTON] = isFgUserSet + } + } + buttonVisibilities[LINK_TO_SETTINGS] = + detailMessage != RequestMessage.NO_MESSAGE + + requestInfos.add(RequestInfo(groupInfo, buttonVisibilities, message, detailMessage)) + } + requestInfos.sortWith(Comparator { rhs, lhs -> + val rhsHasOneTime = rhs.buttonVisibilities[ALLOW_ONE_TIME_BUTTON] + val lhsHasOneTime = lhs.buttonVisibilities[ALLOW_ONE_TIME_BUTTON] + if (rhsHasOneTime && !lhsHasOneTime) { + -1 + } else if (!rhsHasOneTime && lhsHasOneTime) { + 1 + } else { + rhs.groupName.compareTo(lhs.groupName) + } + }) + + value = if (requestInfos.any { it.sendToSettingsImmediately } && + requestInfos.size > 1) { + Log.e(LOG_TAG, "For R+ apps, background permissions must be requested " + + "individually") + null + } else { + requestInfos + } + } + } + + /** + * Converts a list of LightAppPermGroups into a list of GroupStates + */ + private fun getRequiredGroupStates( + groups: List<LightAppPermGroup> + ): MutableMap<Pair<String, Boolean>, GroupState> { + val groupStates = mutableMapOf<Pair<String, Boolean>, GroupState>() + val filteredPermissions = unfilteredAffectedPermissions.filter { perm -> + val group = getGroupWithPerm(perm, groups) + group != null && isPermissionGrantableAndNotFixed(perm, group) + } + for (perm in filteredPermissions) { + val group = getGroupWithPerm(perm, groups)!! + + val isBackground = perm in group.backgroundPermNames + val groupStateInfo = groupStates.getOrPut(group.permGroupName to isBackground) { + GroupState(group, isBackground) + } + + var currGroupState = if (group.permGroupName in permGroupsToSkip) { + STATE_SKIPPED + } else { + groupStateInfo.state + } + if (storedState != null && currGroupState != STATE_UNKNOWN) { + currGroupState = storedState.getInt(getInstanceStateKey(group.permGroupName, + isBackground), STATE_UNKNOWN) + } + + if (currGroupState == STATE_UNKNOWN) { + val otherGroupPermissions = filteredPermissions.filter { it in group.permissions } + currGroupState = getGroupState(perm, group, otherGroupPermissions) + } + + if (currGroupState != STATE_UNKNOWN) { + groupStateInfo.state = currGroupState + } + // If we saved state, load it + groupStateInfo.affectedPermissions.add(perm) + } + return groupStates + } + + /** + * Get the actually requested permissions when a permission is requested. + * + * >In some cases requesting to grant a single permission requires the system to grant + * additional permissions. E.g. before N-MR1 a single permission of a group caused the whole + * group to be granted. Another case are permissions that are split into two. For apps that + * target an SDK before the split, this method automatically adds the split off permission. + * + * @param perm The requested permission + * + * @return The actually requested permissions + */ + private fun computeAffectedPermissions( + perm: String, + appPermissions: Map<String, List<String>> + ): List<String> { + val requestingAppTargetSDK = packageInfo.targetSdkVersion + + // If a permission is split, all permissions the original permission is split into are + // affected + val extendedBySplitPerms = mutableListOf(perm) + + val splitPerms = app.getSystemService(PermissionManager::class.java)!!.splitPermissions + for (splitPerm in splitPerms) { + + if (requestingAppTargetSDK < splitPerm.targetSdk && perm == splitPerm.splitPermission) { + extendedBySplitPerms.addAll(splitPerm.newPermissions) + } + } + + // For <= N_MR1 apps all permissions of the groups of the requested permissions are affected + if (requestingAppTargetSDK <= Build.VERSION_CODES.N_MR1) { + val extendedBySplitPermsAndGroup = mutableListOf<String>() + + for (splitPerm in extendedBySplitPerms) { + val groups = appPermissions.filter { splitPerm in it.value } + if (groups.isEmpty()) { + continue + } + + val permissionsInGroup = groups.values.first() + for (permissionInGroup in permissionsInGroup) { + extendedBySplitPermsAndGroup.add(permissionInGroup) + } + } + + return extendedBySplitPermsAndGroup + } else { + return extendedBySplitPerms + } + } + + private fun isPermissionGrantableAndNotFixed(perm: String, group: LightAppPermGroup): Boolean { + + // If the permission is restricted it does not show in the UI and + // is not added to the group at all, so check that first. + if (perm in group.packageInfo.requestedPermissions && perm !in group.permissions) { + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_RESTRICTED_PERMISSION) + return false + } + + val subGroup = if (perm in group.backgroundPermNames) { + group.background + } else { + group.foreground + } + + val lightPermission = group.permissions[perm] ?: return false + + if (!subGroup.isGrantable) { + reportRequestResult(perm, PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED) + // Skip showing groups that we know cannot be granted. + return false + } else if (subGroup.isUserFixed) { + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_USER_FIXED) + return false + } else if (subGroup.isPolicyFixed && !subGroup.isGranted || lightPermission.isPolicyFixed) { + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED_POLICY_FIXED) + return false + } + + return true + } + + private fun getGroupState( + perm: String, + group: LightAppPermGroup, + otherGroupRequestedPermissions: List<String> + ): Int { + val policyState = getStateFromPolicy(perm, group) + if (policyState != STATE_UNKNOWN) { + return policyState + } + + val isBackground = perm in group.backgroundPermNames + + val hasForegroundRequest = otherGroupRequestedPermissions.any { + it !in group.backgroundPermNames + } + + // Do not attempt to grant background access if foreground access is not either already + // granted or requested + if (isBackground && !group.foreground.isGranted && !hasForegroundRequest) { + Log.w(LOG_TAG, "Cannot grant $perm as the matching foreground permission is not " + + "already granted.") + val affectedPermissions = otherGroupRequestedPermissions.filter { + it in group.backgroundPermNames + } + reportRequestResult(affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__IGNORED) + return STATE_SKIPPED + } + + if (isBackground && group.background.isGranted || + !isBackground && group.foreground.isGranted) { + if (isBackground) { + KotlinUtils.grantBackgroundRuntimePermissions(app, group, listOf(perm)) + } else { + KotlinUtils.grantForegroundRuntimePermissions(app, group, listOf(perm)) + } + KotlinUtils.setGroupFlags(app, group, FLAG_PERMISSION_USER_SET to false, + FLAG_PERMISSION_USER_FIXED to false, filterPermissions = listOf(perm)) + + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED) + return if (storedState == null) { + STATE_SKIPPED + } else { + STATE_ALLOWED + } + } + return STATE_UNKNOWN + } + + private fun getStateFromPolicy(perm: String, group: LightAppPermGroup): Int { + val isBackground = perm in group.backgroundPermNames + var skipGroup = false + var state = STATE_UNKNOWN + when (permissionPolicy) { + DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT -> { + if (isBackground) { + KotlinUtils.grantBackgroundRuntimePermissions(app, group, listOf(perm)) + } else { + KotlinUtils.grantForegroundRuntimePermissions(app, group, listOf(perm)) + } + KotlinUtils.setGroupFlags(app, group, FLAG_PERMISSION_POLICY_FIXED to true, + FLAG_PERMISSION_USER_SET to false, FLAG_PERMISSION_USER_FIXED to false, + filterPermissions = listOf(perm)) + state = STATE_ALLOWED + skipGroup = true + + getAutoGrantNotifier().onPermissionAutoGranted(perm) + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_GRANTED) + } + + DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY -> { + if (group.permissions[perm]?.isPolicyFixed == false) { + KotlinUtils.setGroupFlags(app, group, FLAG_PERMISSION_POLICY_FIXED to true, + FLAG_PERMISSION_USER_SET to false, FLAG_PERMISSION_USER_FIXED to false, + filterPermissions = listOf(perm)) + } + state = STATE_DENIED + skipGroup = true + + reportRequestResult(perm, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__AUTO_DENIED) + } + } + if (skipGroup && storedState == null) { + return STATE_SKIPPED + } + return state + } + + /** + * Upon the user clicking a button, grant permissions, if applicable. + * + * @param groupName The name of the permission group which was changed + * @param result The choice the user made regarding the group. + */ + fun onPermissionGrantResult(groupName: String?, result: Int) { + if (groupName == null) { + return + } + val foregroundGroupState = groupStates[groupName to false] + val backgroundGroupState = groupStates[groupName to true] + when (result) { + GrantPermissionsViewHandler.CANCELED -> { + if (foregroundGroupState != null) { + reportRequestResult(foregroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED) + } + if (backgroundGroupState != null) { + reportRequestResult(backgroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED) + } + return + } + GRANTED_ALWAYS -> { + if (foregroundGroupState != null) { + onPermissionGrantResultSingleState(foregroundGroupState, + granted = true, isOneTime = false, doNotAskAgain = false) + } + if (backgroundGroupState != null) { + onPermissionGrantResultSingleState(backgroundGroupState, + granted = true, isOneTime = false, doNotAskAgain = false) + } + } + GRANTED_FOREGROUND_ONLY -> { + if (foregroundGroupState != null) { + onPermissionGrantResultSingleState(foregroundGroupState, + granted = true, isOneTime = false, doNotAskAgain = false) + } + if (backgroundGroupState != null) { + onPermissionGrantResultSingleState(backgroundGroupState, + granted = false, isOneTime = false, doNotAskAgain = false) + } + } + GrantPermissionsViewHandler.GRANTED_ONE_TIME -> { + if (foregroundGroupState != null) { + onPermissionGrantResultSingleState(foregroundGroupState, + granted = true, isOneTime = true, doNotAskAgain = false) + } + if (backgroundGroupState != null) { + onPermissionGrantResultSingleState(backgroundGroupState, + granted = false, isOneTime = true, doNotAskAgain = false) + } + } + DENIED -> { + if (foregroundGroupState != null) { + onPermissionGrantResultSingleState(foregroundGroupState, + granted = false, isOneTime = false, doNotAskAgain = false) + } + if (backgroundGroupState != null) { + onPermissionGrantResultSingleState(backgroundGroupState, granted = false, + isOneTime = false, doNotAskAgain = false) + } + } + DENIED_DO_NOT_ASK_AGAIN -> { + if (foregroundGroupState != null) { + onPermissionGrantResultSingleState(foregroundGroupState, + granted = false, isOneTime = false, doNotAskAgain = true) + } + if (backgroundGroupState != null) { + onPermissionGrantResultSingleState(backgroundGroupState, + granted = false, isOneTime = false, doNotAskAgain = true) + } + } + } + } + + private fun onPermissionGrantResultSingleState( + groupState: GroupState, + granted: Boolean, + isOneTime: Boolean, + doNotAskAgain: Boolean + ) { + if (groupState.state != STATE_UNKNOWN) { + // We already dealt with this group, don't re-grant/re-revoke + return + } + val result: Int + if (granted) { + result = if (isOneTime) { + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_ONE_TIME + } else { + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED + } + if (groupState.isBackground) { + KotlinUtils.grantBackgroundRuntimePermissions(app, groupState.group, + groupState.affectedPermissions) + } else { + KotlinUtils.grantForegroundRuntimePermissions(app, groupState.group, + groupState.affectedPermissions, isOneTime) + } + groupState.state = STATE_ALLOWED + } else { + if (groupState.isBackground) { + KotlinUtils.revokeBackgroundRuntimePermissions(app, groupState.group, + userFixed = doNotAskAgain, filterPermissions = groupState.affectedPermissions) + } else { + KotlinUtils.revokeForegroundRuntimePermissions(app, groupState.group, + userFixed = doNotAskAgain, filterPermissions = groupState.affectedPermissions) + } + result = if (doNotAskAgain) { + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE + } else { + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED + } + groupState.state = STATE_DENIED + } + reportRequestResult(groupState.affectedPermissions, result) + // group state has changed, reload liveData + requestInfosLiveData.updateIfActive() + } + + private fun getGroupWithPerm( + perm: String, + groups: List<LightAppPermGroup> + ): LightAppPermGroup? { + val groupsWithPerm = groups.filter { perm in it.permissions } + if (groupsWithPerm.isEmpty()) { + return null + } + return groupsWithPerm.first() + } + + /** + * An internal class which represents the state of a current AppPermissionGroup grant request. + */ + internal class GroupState( + internal val group: LightAppPermGroup, + internal val isBackground: Boolean, + internal val affectedPermissions: MutableList<String> = mutableListOf(), + internal var state: Int = STATE_UNKNOWN + ) { + override fun toString(): String { + val stateStr: String = when (state) { + STATE_UNKNOWN -> "unknown" + STATE_ALLOWED -> "granted" + STATE_DENIED -> "denied" + else -> "skipped" + } + return "${group.permGroupName} $isBackground $stateStr $affectedPermissions" + } + } + + private fun reportRequestResult(permissions: List<String>, result: Int) { + for (perm in permissions) { + reportRequestResult(perm, result) + } + } + + /** + * Report the result of a grant of a permission. + * + * @param permission The permission that was granted or denied + * @param result The permission grant result + */ + private fun reportRequestResult(permission: String, result: Int) { + val isImplicit = permission !in requestedPermissions + + Log.v(LOG_TAG, "Permission grant result requestId=$sessionId " + + "callingUid=${packageInfo.uid} callingPackage=$packageName permission=$permission " + + "isImplicit=$isImplicit result=$result") + + PermissionControllerStatsLog.write( + PermissionControllerStatsLog.PERMISSION_GRANT_REQUEST_RESULT_REPORTED, sessionId, + packageInfo.uid, packageName, permission, isImplicit, result) + } + + /** + * Save the group states of the view model, to allow for state restoration after lifecycle + * events + * + * @param outState The bundle in which to store state + */ + fun saveInstanceState(outState: Bundle) { + for ((groupKey, groupState) in groupStates) { + val (groupName, isBackground) = groupKey + outState.putInt(getInstanceStateKey(groupName, isBackground), groupState.state) + } + } + + /** + * Determine if the activity should return permission state to the caller + * + * @return whether or not state should be returned, or true if the calling PackageInfo has not + * been initialized (which it should always be) + */ + fun shouldReturnPermissionState(): Boolean { + return if (packageInfoLiveData.isInitialized) { + packageInfo.targetSdkVersion >= Build.VERSION_CODES.M + } else { + // Should not be reached, as this method shouldn't be called before data is passed to + // the activity for the first time + true + } + } + + /** + * Send the user directly to the AppPermissionFragment. Used for R+ apps. + * + * @param activity The current activity + * @param groupName The name of the permission group whose fragment should be opened + */ + fun sendDirectlyToSettings(activity: Activity, groupName: String) { + if (activityResultCallback == null) { + startAppPermissionFragment(activity, groupName) + activityResultCallback = Consumer { data -> + if (data?.getStringExtra(EXTRA_RESULT_PERMISSION_INTERACTED) == null) { + // User didn't interact, count against rate limit + val group = groupStates[groupName to false]?.group + ?: groupStates[groupName to true]?.group ?: return@Consumer + if (group.background.isUserSet) { + KotlinUtils.setGroupFlags(app, group, FLAG_PERMISSION_USER_FIXED to true, + filterPermissions = group.backgroundPermNames) + } else { + KotlinUtils.setGroupFlags(app, group, FLAG_PERMISSION_USER_SET to true, + filterPermissions = group.backgroundPermNames) + } + } + + permGroupsToSkip.add(groupName) + // Update our liveData now that there is a new skipped group + requestInfosLiveData.updateIfActive() + } + } + } + + /** + * Send the user to the AppPermissionFragment from a link. Used for Q- apps + * + * @param activity The current activity + * @param groupName The name of the permission group whose fragment should be opened + */ + fun sendToSettingsFromLink(activity: Activity, groupName: String) { + startAppPermissionFragment(activity, groupName) + activityResultCallback = Consumer { data -> + val returnGroupName = data?.getStringExtra(EXTRA_RESULT_PERMISSION_INTERACTED) + if (returnGroupName != null) { + permGroupsToSkip.add(returnGroupName) + val result = data.getIntExtra(EXTRA_RESULT_PERMISSION_RESULT, -1) + logSettingsInteraction(returnGroupName, result) + requestInfosLiveData.updateIfActive() + } + } + } + + private fun startAppPermissionFragment(activity: Activity, groupName: String) { + val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSION) + .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) + .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName) + .putExtra(Intent.EXTRA_USER, user) + .putExtra(ManagePermissionsActivity.EXTRA_CALLER_NAME, + GrantPermissionsActivity::class.java.name) + .putExtra(Constants.EXTRA_SESSION_ID, sessionId) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + activity.startActivityForResult(intent, APP_PERMISSION_REQUEST_CODE) + } + + private fun getInstanceStateKey(groupName: String, isBackground: Boolean): String { + return "${this::class.java.name}_${groupName}_$isBackground" + } + + private fun logSettingsInteraction(groupName: String, result: Int) { + val foregroundGroupState = groupStates[groupName to false] + val backgroundGroupState = groupStates[groupName to true] + val deniedPrejudiceInSettings = + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_PREJUDICE_IN_SETTINGS + when (result) { + GRANTED_ALWAYS -> { + if (foregroundGroupState != null) { + reportRequestResult(foregroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS) + } + if (backgroundGroupState != null) { + reportRequestResult(backgroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS) + } + } + GRANTED_FOREGROUND_ONLY -> { + if (foregroundGroupState != null) { + reportRequestResult(foregroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_IN_SETTINGS) + } + if (backgroundGroupState != null) { + reportRequestResult(backgroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS) + } + } + DENIED -> { + if (foregroundGroupState != null) { + reportRequestResult(foregroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS) + } + if (backgroundGroupState != null) { + reportRequestResult(backgroundGroupState.affectedPermissions, + PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_IN_SETTINGS) + } + } + DENIED_DO_NOT_ASK_AGAIN -> { + if (foregroundGroupState != null) { + reportRequestResult(foregroundGroupState.affectedPermissions, + deniedPrejudiceInSettings) + } + if (backgroundGroupState != null) { + reportRequestResult(backgroundGroupState.affectedPermissions, + deniedPrejudiceInSettings) + } + } + } + } + + /** + * Log all permission groups which were requested + */ + fun logRequestedPermissionGroups() { + if (groupStates.isEmpty()) { + return + } + val groups = groupStates.map { it.value.group } + SafetyNetLogger.logPermissionsRequested(packageName, packageInfo.uid, groups) + } + + /** + * Log information about the buttons which were shown and clicked by the user. + * + * @param groupName The name of the permission group which was interacted with + * @param clickedButton The button that was clicked by the user + * @param presentedButtons All buttons which were shown to the user + */ + fun logClickedButtons(groupName: String?, clickedButton: Int, presentedButtons: Int) { + if (groupName == null) { + return + } + PermissionControllerStatsLog.write(GRANT_PERMISSIONS_ACTIVITY_BUTTON_ACTIONS, + groupName, packageInfo.uid, packageName, presentedButtons, clickedButton, sessionId) + Log.v(LOG_TAG, "Logged buttons presented and clicked permissionGroupName=" + + "$groupName uid=${packageInfo.uid} package=$packageName presentedButtons=" + + "$presentedButtons clickedButton=$clickedButton sessionId=$sessionId") + } + + /** + * Use the autoGrantNotifier to notify of auto-granted permissions. + */ + fun autoGrantNotify() { + autoGrantNotifier?.notifyOfAutoGrantPermissions(true) + } + + companion object { + private const val APP_PERMISSION_REQUEST_CODE = 1 + private const val STATE_UNKNOWN = 0 + private const val STATE_ALLOWED = 1 + private const val STATE_DENIED = 2 + private const val STATE_SKIPPED = 3 + + /** + * An enum that represents the type of message which should be shown- foreground, + * background, upgrade, or no message. + */ + enum class RequestMessage(request: Int) { + FG_MESSAGE(0), + BG_MESSAGE(1), + UPGRADE_MESSAGE(2), + NO_MESSAGE(3) + } + } +} + +/** + * Factory for an AppPermissionViewModel + * + * @param app The current application + * @param packageName The name of the package this ViewModel represents + */ +class GrantPermissionsViewModelFactory( + private val app: Application, + private val packageName: String, + private val requestedPermissions: Array<String>, + private val sessionId: Long, + private val savedState: Bundle? +) : ViewModelProvider.Factory { + override fun <T : ViewModel> create(modelClass: Class<T>): T { + @Suppress("UNCHECKED_CAST") + return GrantPermissionsViewModel(app, packageName, requestedPermissions.toList(), sessionId, + savedState) as T + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt index 64068fe41..ebf23b099 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt @@ -46,6 +46,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.os.UserHandle +import android.permission.PermissionManager import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.Observer @@ -56,6 +57,7 @@ import com.android.permissioncontroller.R import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData import com.android.permissioncontroller.permission.data.get import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup +import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo import com.android.permissioncontroller.permission.model.livedatatypes.LightPermission import com.android.permissioncontroller.permission.model.livedatatypes.PermState import com.android.permissioncontroller.permission.service.LocationAccessCheck @@ -87,6 +89,20 @@ object KotlinUtils { private const val KILL_REASON_APP_OP_CHANGE = "Permission related app op changed" /** + * Importance level to define the threshold for whether a package is in a state which resets the + * timer on its one-time permission session + */ + private val ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER = + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + + /** + * Importance level to define the threshold for whether a package is in a state which keeps its + * one-time permission session alive after the timer ends + */ + private val ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE = + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE + + /** * Given a Map, and a List, determines which elements are in the list, but not the map, and * vice versa. Used primarily for determining which liveDatas are already being watched, and * which need to be removed or added @@ -365,6 +381,50 @@ object KotlinUtils { } /** + * Set a list of flags for a set of permissions of a LightAppPermGroup + * + * @param app: The current application + * @param group: The LightAppPermGroup whose permission flags we wish to set + * @param flags: Pairs of <FlagInt, ShouldSetFlag> + * @param filterPermissions: A list of permissions to filter by. Only the filtered permissions + * will be set + * + * @return A new LightAppPermGroup with the flags set. + */ + fun setGroupFlags( + app: Application, + group: LightAppPermGroup, + vararg flags: Pair<Int, Boolean>, + filterPermissions: List<String> = group.permissions.keys.toList() + ): LightAppPermGroup { + var flagMask = 0 + var flagsToSet = 0 + for ((flag, shouldSet) in flags) { + flagMask = flagMask or flag + if (shouldSet) { + flagsToSet = flagsToSet or flag + } + } + + val permFlagFilter = flagsToSet or flagMask.inv() + val newPerms = mutableMapOf<String, LightPermission>() + for ((permName, perm) in group.permissions) { + if (permName !in filterPermissions) { + continue + } + val newFlags = perm.flags and permFlagFilter + if (newFlags != perm.flags) { + app.packageManager.updatePermissionFlags(permName, group.packageName, + group.userHandle, *flags) + } + newPerms[permName] = LightPermission(group.packageInfo, perm.permInfo, + perm.isGrantedIncludingAppOp, newFlags, perm.foregroundPerms) + } + return LightAppPermGroup(group.packageInfo, group.permGroupInfo, newPerms, + group.hasInstallToRuntimeSplit, group.specialLocationGrant) + } + + /** * Grant all foreground runtime permissions of a LightAppPermGroup * * <p>This also automatically grants all app ops for permissions that have app ops. @@ -381,9 +441,10 @@ object KotlinUtils { fun grantForegroundRuntimePermissions( app: Application, group: LightAppPermGroup, - filterPermissions: List<String> = group.permissions.keys.toList() + filterPermissions: List<String> = group.permissions.keys.toList(), + isOneTime: Boolean = false ): LightAppPermGroup { - return grantRuntimePermissions(app, group, false, filterPermissions) + return grantRuntimePermissions(app, group, false, isOneTime, filterPermissions) } /** @@ -405,22 +466,24 @@ object KotlinUtils { group: LightAppPermGroup, filterPermissions: List<String> = group.permissions.keys.toList() ): LightAppPermGroup { - return grantRuntimePermissions(app, group, true, filterPermissions) + return grantRuntimePermissions(app, group, true, false, filterPermissions) } private fun grantRuntimePermissions( app: Application, group: LightAppPermGroup, grantBackground: Boolean, + isOneTime: Boolean = false, filterPermissions: List<String> = group.permissions.keys.toList() ): LightAppPermGroup { + val wasOneTime = group.isOneTime val newPerms = group.permissions.toMutableMap() var shouldKillForAnyPermission = false for (permName in filterPermissions) { val perm = group.permissions[permName] ?: continue val isBackgroundPerm = permName in group.backgroundPermNames if (isBackgroundPerm == grantBackground) { - val (newPerm, shouldKill) = grantRuntimePermission(app, perm, group) + val (newPerm, shouldKill) = grantRuntimePermission(app, perm, isOneTime, group) newPerms[newPerm.name] = newPerm shouldKillForAnyPermission = shouldKillForAnyPermission || shouldKill } @@ -430,8 +493,15 @@ object KotlinUtils { (app.getSystemService(ActivityManager::class.java) as ActivityManager).killUid( group.packageInfo.uid, KILL_REASON_APP_OP_CHANGE) } - return LightAppPermGroup(group.packageInfo, group.permGroupInfo, newPerms, + val newGroup = LightAppPermGroup(group.packageInfo, group.permGroupInfo, newPerms, group.hasInstallToRuntimeSplit, group.specialLocationGrant) + if (newGroup.isOneTime) { + app.getSystemService(PermissionManager::class.java)!!.startOneTimePermissionSession( + group.packageName, Utils.getOneTimePermissionsTimeout(), + ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_RESET_TIMER, + ONE_TIME_PACKAGE_IMPORTANCE_LEVEL_TO_KEEP_SESSION_ALIVE) + } + return newGroup } /** @@ -448,6 +518,7 @@ object KotlinUtils { private fun grantRuntimePermission( app: Application, perm: LightPermission, + isOneTime: Boolean, group: LightAppPermGroup ): Pair<LightPermission, Boolean> { val user = UserHandle.getUserHandleForUid(group.packageInfo.uid) @@ -497,9 +568,14 @@ object KotlinUtils { // no longer has it fixed in a denied state. newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_USER_FIXED) newFlags = newFlags.setFlag(PackageManager.FLAG_PERMISSION_USER_SET) - newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME) newFlags = newFlags.clearFlag(PackageManager.FLAG_PERMISSION_AUTO_REVOKED) + newFlags = if (isOneTime) { + newFlags.setFlag(PackageManager.FLAG_PERMISSION_ONE_TIME) + } else { + newFlags.clearFlag(PackageManager.FLAG_PERMISSION_ONE_TIME) + } + // If we newly grant background access to the fine location, double-guess the user some // time later if this was really the right choice. if (!perm.isGrantedIncludingAppOp && isGranted) { @@ -586,6 +662,7 @@ object KotlinUtils { oneTime: Boolean, filterPermissions: List<String> ): LightAppPermGroup { + val wasOneTime = group.isOneTime val newPerms = group.permissions.toMutableMap() var shouldKillForAnyPermission = false for (permName in filterPermissions) { @@ -603,11 +680,51 @@ object KotlinUtils { (app.getSystemService(ActivityManager::class.java) as ActivityManager).killUid( group.packageInfo.uid, KILL_REASON_APP_OP_CHANGE) } - return LightAppPermGroup(group.packageInfo, group.permGroupInfo, newPerms, + + val newGroup = LightAppPermGroup(group.packageInfo, group.permGroupInfo, newPerms, group.hasInstallToRuntimeSplit, group.specialLocationGrant) + + if (wasOneTime && !anyPermsOfPackageOneTimeGranted(app, newGroup.packageInfo, newGroup)) { + app.getSystemService(PermissionManager::class.java)!!.stopOneTimePermissionSession( + group.packageName) + } + return newGroup } /** + * Determines if any permissions of a package are granted for one-time only + * + * @param app The current application + * @param packageInfo The packageInfo we wish to examine + * @param group Optional, the current app permission group we are examining + * + * @return true if any permission in the package is granted for one time, false otherwise + */ + private fun anyPermsOfPackageOneTimeGranted( + app: Application, + packageInfo: LightPackageInfo, + group: LightAppPermGroup? = null + ): Boolean { + val user = group?.userHandle ?: UserHandle.getUserHandleForUid(packageInfo.uid) + if (group?.isOneTime == true) { + return true + } + for ((idx, permName) in packageInfo.requestedPermissions.withIndex()) { + if (permName in group?.permissions ?: emptyMap()) { + continue + } + val flags = app.packageManager.getPermissionFlags(permName, packageInfo.packageName, + user) and FLAG_PERMISSION_ONE_TIME + val granted = packageInfo.requestedPermissionsFlags[idx] == + PackageManager.PERMISSION_GRANTED && + (flags and FLAG_PERMISSION_REVOKED_COMPAT) == 0 + if (granted && (flags and FLAG_PERMISSION_ONE_TIME) != 0) { + return true + } + } + return false + } + /** * Revokes a single runtime permission. * * @param app The current application diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/SafetyNetLogger.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/SafetyNetLogger.java index ec2d261be..f0227cad5 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/SafetyNetLogger.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/SafetyNetLogger.java @@ -44,6 +44,14 @@ public final class SafetyNetLogger { /* do nothing */ } + /** + * Log that permission groups have been requested for the purpose of safety net. + * + * <p>The groups might refer to different permission groups and different apps. + * + * @param packageInfo The info about the package for which permissions were requested + * @param groups The permission groups which were requested + */ public static void logPermissionsRequested(PackageInfo packageInfo, List<AppPermissionGroup> groups) { EventLog.writeEvent(SNET_NET_EVENT_LOG_TAG, PERMISSIONS_REQUESTED, @@ -52,6 +60,21 @@ public final class SafetyNetLogger { } /** + * Log that permission groups have been requested for the purpose of safety net. + * + * <p>The groups might refer to different permission groups and different apps. + * + * @param packageName The name of the package for which permissions were requested + * @param uid The uid of the package + * @param groups The permission groups which were requested + */ + public static void logPermissionsRequested(String packageName, int uid, + List<LightAppPermGroup> groups) { + EventLog.writeEvent(SNET_NET_EVENT_LOG_TAG, PERMISSIONS_REQUESTED, uid, + buildChangedPermissionForPackageMessageNew(packageName, groups)); + } + + /** * Log that permission groups have been toggled for the purpose of safety net. * * <p>The groups might refer to different permission groups and different apps. @@ -106,9 +129,10 @@ public final class SafetyNetLogger { * background */ public static void logPermissionToggled(LightAppPermGroup group, boolean logOnlyBackground) { + StringBuilder builder = new StringBuilder(); + buildChangedPermissionForGroup(group, logOnlyBackground, builder); EventLog.writeEvent(SNET_NET_EVENT_LOG_TAG, PERMISSIONS_TOGGLED, - android.os.Process.myUid(), buildChangedPermissionForPackageMessage(group, - logOnlyBackground)); + android.os.Process.myUid(), builder.toString()); } /** @@ -122,9 +146,8 @@ public final class SafetyNetLogger { logPermissionToggled(group, false); } - private static String buildChangedPermissionForPackageMessage( - LightAppPermGroup group, boolean logOnlyBackground) { - StringBuilder builder = new StringBuilder(); + private static void buildChangedPermissionForGroup(LightAppPermGroup group, + boolean logOnlyBackground, StringBuilder builder) { builder.append(group.getPackageInfo().getPackageName()).append(':'); @@ -142,8 +165,6 @@ public final class SafetyNetLogger { builder.append(permission.isGrantedIncludingAppOp()).append('|'); builder.append(permission.getFlags()); } - - return builder.toString(); } private static void buildChangedPermissionForGroup(AppPermissionGroup group, @@ -162,6 +183,17 @@ public final class SafetyNetLogger { } } + private static String buildChangedPermissionForPackageMessageNew(String packageName, + List<LightAppPermGroup> groups) { + StringBuilder builder = new StringBuilder(); + + builder.append(packageName).append(':'); + for (LightAppPermGroup group: groups) { + buildChangedPermissionForGroup(group, false, builder); + } + return builder.toString(); + } + private static String buildChangedPermissionForPackageMessage(String packageName, List<AppPermissionGroup> groups) { StringBuilder builder = new StringBuilder(); diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java index 65fdd590a..95a28c4f0 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/Utils.java @@ -29,6 +29,8 @@ import static android.Manifest.permission_group.PHONE; import static android.Manifest.permission_group.SENSORS; import static android.Manifest.permission_group.SMS; import static android.Manifest.permission_group.STORAGE; +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE; import static android.content.Context.MODE_PRIVATE; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT; import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; @@ -43,6 +45,7 @@ import static android.os.UserHandle.myUserId; import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID; import android.Manifest; +import android.app.AppOpsManager; import android.app.Application; import android.app.role.RoleManager; import android.content.ActivityNotFoundException; @@ -51,9 +54,11 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -62,6 +67,7 @@ import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Parcelable; import android.os.Process; import android.os.UserHandle; @@ -719,15 +725,23 @@ public final class Utils { * Get the message shown to grant a permission group to an app. * * @param appLabel The label of the app - * @param group the group to be granted + * @param packageName The package name of the app + * @param groupName The name of the permission group * @param context A context to resolve resources * @param requestRes The resource id of the grant request message * * @return The formatted message to be used as title when granting permissions */ - public static CharSequence getRequestMessage(CharSequence appLabel, AppPermissionGroup group, - Context context, @StringRes int requestRes) { - if (group.getName().equals(STORAGE) && !group.isNonIsolatedStorage()) { + public static CharSequence getRequestMessage(CharSequence appLabel, String packageName, + String groupName, Context context, @StringRes int requestRes) { + + boolean isIsolatedStorage = false; + try { + isIsolatedStorage = !isNonIsolatedStorage(context, packageName); + } catch (NameNotFoundException e) { + return null; + } + if (groupName.equals(STORAGE) && isIsolatedStorage) { return Html.fromHtml( String.format(context.getResources().getConfiguration().getLocales().get(0), context.getString(R.string.permgrouprequest_storage_isolated), @@ -737,7 +751,43 @@ public final class Utils { } return Html.fromHtml(context.getString(R.string.permission_warning_template, appLabel, - group.getDescription()), 0); + loadGroupDescription(context, groupName, context.getPackageManager())), 0); + } + + private static CharSequence loadGroupDescription(Context context, String groupName, + @NonNull PackageManager packageManager) { + PackageItemInfo groupInfo = getGroupInfo(groupName, context); + CharSequence description = null; + if (groupInfo instanceof PermissionGroupInfo) { + description = ((PermissionGroupInfo) groupInfo).loadDescription(packageManager); + } else if (groupInfo instanceof PermissionInfo) { + description = ((PermissionInfo) groupInfo).loadDescription(packageManager); + } + + if (description == null || description.length() <= 0) { + description = context.getString(R.string.default_permission_description); + } + + return description; + } + + /** + * Whether or not the given package has non-isolated storage permissions + * @param context The current context + * @param packageName The package name to check + * @return True if the package has access to non-isolated storage, false otherwise + * @throws NameNotFoundException + */ + public static boolean isNonIsolatedStorage(@NonNull Context context, + @NonNull String packageName) throws NameNotFoundException { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0); + AppOpsManager manager = context.getSystemService(AppOpsManager.class); + + + return packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.P + || (packageInfo.applicationInfo.targetSdkVersion < Build.VERSION_CODES.R + && manager.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, + packageInfo.applicationInfo.uid, packageInfo.packageName) == MODE_ALLOWED); } /** diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/permission/utils/GrantRevokeTests.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/permission/utils/GrantRevokeTests.kt index a43f25cfd..fa63fe969 100644 --- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/permission/utils/GrantRevokeTests.kt +++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/permission/utils/GrantRevokeTests.kt @@ -39,6 +39,7 @@ import android.content.pm.PermissionInfo.PROTECTION_FLAG_INSTANT import android.content.pm.PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY import android.os.Build import android.os.UserHandle +import android.permission.PermissionManager import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo import com.android.permissioncontroller.permission.model.livedatatypes.LightPermGroupInfo @@ -120,6 +121,9 @@ class GrantRevokeTests { `when`(app.getSystemService(ActivityManager::class.java)).thenReturn( mock(ActivityManager::class.java)) + + `when`(app.getSystemService(PermissionManager::class.java)).thenReturn( + mock(PermissionManager::class.java)) } /** @@ -742,7 +746,7 @@ class GrantRevokeTests { fun revokeTwoPermTest() { val pkg = createMockPackage(mapOf(FG_PERM_NAME to true, FG_PERM_2_NAME to true)) val perms = mutableMapOf<String, LightPermission>() - perms[FG_PERM_NAME] = createMockPerm(pkg,FG_PERM_NAME) + perms[FG_PERM_NAME] = createMockPerm(pkg, FG_PERM_NAME) perms[FG_PERM_2_NAME] = createMockPerm(pkg, FG_PERM_2_NAME) val group = createMockGroup(pkg, perms) resetMockAppState() |