diff options
11 files changed, 413 insertions, 52 deletions
diff --git a/PermissionController/res/values-v34/strings.xml b/PermissionController/res/values-v34/strings.xml index 412467779..09f2e17a3 100644 --- a/PermissionController/res/values-v34/strings.xml +++ b/PermissionController/res/values-v34/strings.xml @@ -37,7 +37,7 @@ may share for app functionality, analytics, developer communications, advertising or marketing, fraud prevention, security, and compliance, personalization, account management. This may be updated in the future." [CHAR LIMIT=300] --> - <string name="permission_rationale_purpose_message">This app developer stated to <annotation id="link"><xliff:g id="install_source" example="App Store">%1$s</xliff:g></annotation> that it may share for <xliff:g id="purpose_list" example="purpose 1, purpose 2, purpose 3">%2$s</xliff:g>\nThis may be updated in the future.</string> + <string name="permission_rationale_purpose_message">This app developer stated to <annotation id="link"><annotation id="install_source" example="App Store">%1$s</annotation></annotation> that it may share for <annotation id="purpose_list" example="purpose 1, purpose 2, purpose 3">%2$s</annotation>\nThis may be updated in the future.</string> <!-- TODO(b/259279178): update with finalized permission rationale strings --> <!-- Message shown to the user letting them know that data will be shared and for which diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/LightInstallSourceInfoLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/LightInstallSourceInfoLiveData.kt new file mode 100644 index 000000000..543d8eae2 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/LightInstallSourceInfoLiveData.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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.data + +import android.app.Application +import android.content.Context +import android.content.pm.InstallSourceInfo +import android.content.pm.PackageManager +import android.os.UserHandle +import android.util.Log +import com.android.permissioncontroller.PermissionControllerApplication +import com.android.permissioncontroller.permission.model.livedatatypes.LightInstallSourceInfo +import com.android.permissioncontroller.permission.model.livedatatypes.LightInstallSourceInfo.Companion.UNKNOWN_INSTALL_SOURCE +import com.android.permissioncontroller.permission.utils.Utils +import kotlinx.coroutines.Job + +/** + * [LightInstallSourceInfo] [LiveData] for the specified package + * + * @param app current Application + * @param packageName name of the package to get InstallSourceInfo for + * @param user The user of the package + */ +class LightInstallSourceInfoLiveData +private constructor( + private val app: Application, + private val packageName: String, + private val user: UserHandle +) : SmartAsyncMediatorLiveData<LightInstallSourceInfo>(), + PackageBroadcastReceiver.PackageBroadcastListener { + + override fun onActive() { + super.onActive() + PackageBroadcastReceiver.addChangeCallback(packageName, this) + } + + override fun onInactive() { + super.onInactive() + PackageBroadcastReceiver.removeChangeCallback(packageName, this) + } + + /** + * Callback from the PackageBroadcastReceiver + * + * @param packageName the name of the package which was updated. + */ + override fun onPackageUpdate(packageName: String) { + update() + } + + override suspend fun loadDataAndPostValue(job: Job) { + if (job.isCancelled) { + return + } + + val lightInstallSourceInfo: LightInstallSourceInfo = + try { + val userContext = Utils.getUserContext(app, user) + LightInstallSourceInfo( + getInstallSourceInfo(userContext, packageName).installingPackageName) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_TAG, "InstallSourceInfo for $packageName not found") + SafetyLabelInfoLiveData.invalidateSingle(packageName to user) + UNKNOWN_INSTALL_SOURCE + } + postValue(lightInstallSourceInfo) + } + + companion object : + DataRepositoryForPackage<Pair<String, UserHandle>, LightInstallSourceInfoLiveData>() { + private val LOG_TAG = LightInstallSourceInfoLiveData::class.java.simpleName + + override fun newValue(key: Pair<String, UserHandle>): LightInstallSourceInfoLiveData { + return LightInstallSourceInfoLiveData( + PermissionControllerApplication.get(), key.first, key.second) + } + + /** Returns the [InstallSourceInfo] for the given package */ + @Throws(PackageManager.NameNotFoundException::class) + private fun getInstallSourceInfo(context: Context, packageName: String): InstallSourceInfo { + return context.packageManager.getInstallSourceInfo(packageName) + } + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt index 9dc16e306..c66e7a7d6 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/PackageBroadcastReceiver.kt @@ -161,6 +161,8 @@ object PackageBroadcastReceiver : BroadcastReceiver() { HibernationSettingStateLiveData.invalidateAllForPackage(packageName) LightAppPermGroupLiveData.invalidateAllForPackage(packageName) AppPermGroupUiInfoLiveData.invalidateAllForPackage(packageName) + SafetyLabelInfoLiveData.invalidateAllForPackage(packageName) + LightInstallSourceInfoLiveData.invalidateAllForPackage(packageName) } } @@ -176,4 +178,4 @@ object PackageBroadcastReceiver : BroadcastReceiver() { */ fun onPackageUpdate(packageName: String) } -}
\ No newline at end of file +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/SafetyLabelLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/SafetyLabelInfoLiveData.kt index 48909384f..64bd21e13 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/SafetyLabelLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/SafetyLabelInfoLiveData.kt @@ -19,55 +19,93 @@ package com.android.permissioncontroller.permission.data import android.app.Application import android.content.pm.PackageManager import android.os.PersistableBundle +import android.os.UserHandle import android.util.Log import com.android.permission.safetylabel.DataCategoryConstants import com.android.permission.safetylabel.DataLabelConstants import com.android.permission.safetylabel.DataTypeConstants import com.android.permission.safetylabel.SafetyLabel import com.android.permissioncontroller.PermissionControllerApplication -import com.android.permissioncontroller.permission.utils.KotlinUtils.isPermissionRationaleEnabled +import com.android.permissioncontroller.permission.model.livedatatypes.SafetyLabelInfo import com.android.permissioncontroller.permission.utils.KotlinUtils.isPlaceholderSafetyLabelDataEnabled import kotlinx.coroutines.Job /** - * SafetyLabel LiveData for the specified package + * [SafetyLabelInfo] [LiveData] for the specified package * * @param app current Application * @param packageName name of the package to get SafetyLabel information for + * @param user The user of the package */ -class SafetyLabelLiveData -private constructor(private val app: Application, private val packageName: String) : - SmartAsyncMediatorLiveData<SafetyLabel>() { +class SafetyLabelInfoLiveData +private constructor( + private val app: Application, + private val packageName: String, + private val user: UserHandle +) : + SmartAsyncMediatorLiveData<SafetyLabelInfo>(), + PackageBroadcastReceiver.PackageBroadcastListener { + + private val lightInstallSourceInfoLiveData = LightInstallSourceInfoLiveData[packageName, user] + + init { + addSource(lightInstallSourceInfoLiveData) { update() } + + update() + } + + override fun onActive() { + super.onActive() + PackageBroadcastReceiver.addChangeCallback(packageName, this) + } + + override fun onInactive() { + super.onInactive() + PackageBroadcastReceiver.removeChangeCallback(packageName, this) + } + + /** + * Callback from the PackageBroadcastReceiver + * + * @param packageName the name of the package which was updated. + */ + override fun onPackageUpdate(packageName: String) { + update() + } override suspend fun loadDataAndPostValue(job: Job) { if (job.isCancelled) { return } - if (!isPermissionRationaleEnabled()) { - postValue(null) + if (lightInstallSourceInfoLiveData.isStale) { return } - if (packageName.isEmpty()) { - postValue(null) + // TODO(b/261607291): Add support preinstall apps that provide SafetyLabel. Installing + // package is null until updated from an app store + val installSourcePackageName = lightInstallSourceInfoLiveData.value?.installingPackageName + if (installSourcePackageName == null) { + postValue(SafetyLabelInfo.UNAVAILABLE) return } - val safetyLabel: SafetyLabel? = + val safetyLabelInfo: SafetyLabelInfo = try { - val metadataBundle: PersistableBundle? = getInstallMetadataBundle() - SafetyLabel.getSafetyLabelFromMetadata(metadataBundle) + val metadataBundle: PersistableBundle? = getAppMetadata() + SafetyLabelInfo( + SafetyLabel.getSafetyLabelFromMetadata(metadataBundle), + installSourcePackageName) } catch (e: PackageManager.NameNotFoundException) { Log.w(LOG_TAG, "SafetyLabel for $packageName not found") - invalidateSingle(packageName) - null + invalidateSingle(packageName to user) + SafetyLabelInfo.UNAVAILABLE } - postValue(safetyLabel) + postValue(safetyLabelInfo) } // TODO(b/257293222): Update when hooking up PackageManager APIs - private fun getInstallMetadataBundle(): PersistableBundle? { + private fun getAppMetadata(): PersistableBundle? { return if (isPlaceholderSafetyLabelDataEnabled()) { placeholderMetadataBundle() } else { @@ -106,11 +144,13 @@ private constructor(private val app: Application, private val packageName: Strin } } - companion object : DataRepositoryForPackage<String, SafetyLabelLiveData>() { - private val LOG_TAG = SafetyLabelLiveData::class.java.simpleName + companion object : DataRepositoryForPackage<Pair<String, UserHandle>, SafetyLabelInfoLiveData>( + ) { + private val LOG_TAG = SafetyLabelInfoLiveData::class.java.simpleName - override fun newValue(key: String): SafetyLabelLiveData { - return SafetyLabelLiveData(PermissionControllerApplication.get(), key) + override fun newValue(key: Pair<String, UserHandle>): SafetyLabelInfoLiveData { + return SafetyLabelInfoLiveData(PermissionControllerApplication.get(), key.first, + key.second) } } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightInstallSourceInfo.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightInstallSourceInfo.kt new file mode 100644 index 000000000..68fdf8739 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/LightInstallSourceInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 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.model.livedatatypes + +/** + * A lighter version of the system's InstallSourceInfo class, containing select information about + * the install source. + * + * @param installingPackageName The package name of the install source (usually the app store) + */ +class LightInstallSourceInfo(val installingPackageName: String?) { + + companion object { + val UNKNOWN_INSTALL_SOURCE = LightInstallSourceInfo(null) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/SafetyLabelInfo.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/SafetyLabelInfo.kt new file mode 100644 index 000000000..2c26ad0d4 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/SafetyLabelInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.model.livedatatypes + +import com.android.permission.safetylabel.SafetyLabel + +/** + * A wrapping class for [SafetyLabel] class that includes the install source package name + * + * @param safetyLabel The resulting [SafetyLabel], or null if none found + * @param installSourcePackageName The package name of the install source for the APK and safety + * label(usually the app store) + */ +class SafetyLabelInfo(val safetyLabel: SafetyLabel?, val installSourcePackageName: String?) { + + companion object { + /** Default definition of unavailable or no safety label found */ + val UNAVAILABLE = SafetyLabelInfo(null, null) + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt index a942343af..a474bab7f 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/GrantPermissionsViewModel.kt @@ -74,7 +74,7 @@ import com.android.permissioncontroller.auto.DrivingDecisionReminderService 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.SafetyLabelLiveData +import com.android.permissioncontroller.permission.data.SafetyLabelInfoLiveData import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData import com.android.permissioncontroller.permission.data.get import com.android.permissioncontroller.permission.model.livedatatypes.LightAppPermGroup @@ -152,11 +152,12 @@ class GrantPermissionsViewModel( private val LOG_TAG = GrantPermissionsViewModel::class.java.simpleName private val user = Process.myUserHandle() private val packageInfoLiveData = LightPackageInfoLiveData[packageName, user] - private val safetyLabelLiveData = SafetyLabelLiveData[packageName] + private val safetyLabelInfoLiveData = SafetyLabelInfoLiveData[packageName, user] private val dpm = app.getSystemService(DevicePolicyManager::class.java)!! private val permissionPolicy = dpm.getPermissionPolicy(null) private val permGroupsToSkip = mutableListOf<String>() private var groupStates = mutableMapOf<Pair<String, Boolean>, GroupState>() + private val permissionRationaleEnabled: Boolean by lazy { isPermissionRationaleEnabled() } private var autoGrantNotifier: AutoGrantPermissionsNotifier? = null private fun getAutoGrantNotifier(): AutoGrantPermissionsNotifier { @@ -202,10 +203,14 @@ class GrantPermissionsViewModel( private val LOG_TAG = GrantPermissionsViewModel::class.java.simpleName private val packagePermissionsLiveData = PackagePermissionsLiveData[packageName, user] + // TODO(b/260873483): only query safety label for supported permission groups. should only + // query location, but currently queries for all groups init { addSource(packagePermissionsLiveData) { onPackageLoaded() } addSource(packageInfoLiveData) { onPackageLoaded() } - addSource(safetyLabelLiveData) { onPackageLoaded() } + if (permissionRationaleEnabled) { + addSource(safetyLabelInfoLiveData) { onPackageLoaded() } + } // Load package state, if available onPackageLoaded() @@ -214,10 +219,17 @@ class GrantPermissionsViewModel( private fun onPackageLoaded() { if (packageInfoLiveData.isStale || packagePermissionsLiveData.isStale || - safetyLabelLiveData.isStale) { + (permissionRationaleEnabled && safetyLabelInfoLiveData.isStale)) { return } + safetyLabel = + if (permissionRationaleEnabled) { + safetyLabelInfoLiveData.value?.safetyLabel + } else { + null + } + val groups = packagePermissionsLiveData.value val pI = packageInfoLiveData.value if (groups == null || groups.isEmpty() || pI == null) { @@ -235,8 +247,6 @@ class GrantPermissionsViewModel( return } - safetyLabel = safetyLabelLiveData.value - val allAffectedPermissions = requestedPermissions.toMutableSet() for (requestedPerm in requestedPermissions) { allAffectedPermissions.addAll(computeAffectedPermissions(requestedPerm, groups)) @@ -610,9 +620,7 @@ class GrantPermissionsViewModel( safetyLabel: SafetyLabel?, groupState: GroupState ): Boolean { - if (!isPermissionRationaleEnabled() || - safetyLabel == null || - safetyLabel.dataLabel.dataShared.isEmpty()) { + if (safetyLabel == null || safetyLabel.dataLabel.dataShared.isEmpty()) { return false } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt index c88e8a7b6..eb9c5bad8 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/model/v34/PermissionRationaleViewModel.kt @@ -17,8 +17,10 @@ package com.android.permissioncontroller.permission.ui.model.v34 import android.app.Application +import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.Process import android.util.Log import androidx.core.util.Consumer import androidx.lifecycle.ViewModel @@ -27,8 +29,12 @@ import com.android.permission.safetylabel.DataCategory import com.android.permission.safetylabel.DataType import com.android.permission.safetylabel.DataTypeConstants import com.android.permission.safetylabel.SafetyLabel -import com.android.permissioncontroller.permission.data.SafetyLabelLiveData +import com.android.permissioncontroller.permission.data.SafetyLabelInfoLiveData import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData +import com.android.permissioncontroller.permission.data.get +import com.android.permissioncontroller.permission.model.livedatatypes.SafetyLabelInfo.Companion.UNAVAILABLE +import com.android.permissioncontroller.permission.utils.KotlinUtils +import com.android.permissioncontroller.permission.utils.KotlinUtils.getAppStoreIntent import com.android.permissioncontroller.permission.utils.SafetyLabelPermissionMapping /** @@ -49,7 +55,8 @@ class PermissionRationaleViewModel( private val sessionId: Long, private val storedState: Bundle? ) : ViewModel() { - private val safetyLabelLiveData = SafetyLabelLiveData[packageName] + private val user = Process.myUserHandle() + private val safetyLabelInfoLiveData = SafetyLabelInfoLiveData[packageName, user] var activityResultCallback: Consumer<Intent>? = null @@ -59,7 +66,8 @@ class PermissionRationaleViewModel( */ data class PermissionRationaleInfo( val groupName: String, - val installSourceName: String?, + val installSourcePackageName: String?, + val installSourceLabel: CharSequence?, val purposeSet: Set<Int> ) @@ -68,29 +76,39 @@ class PermissionRationaleViewModel( object : SmartUpdateMediatorLiveData<PermissionRationaleInfo>() { init { - addSource(safetyLabelLiveData) { onUpdate() } + addSource(safetyLabelInfoLiveData) { onUpdate() } // Load package state, if available onUpdate() } override fun onUpdate() { - if (safetyLabelLiveData.isStale) { + if (safetyLabelInfoLiveData.isStale) { return } - val safetyLabel = safetyLabelLiveData.value - if (safetyLabel == null) { + val safetyLabelInfo = safetyLabelInfoLiveData.value + val safetyLabel = safetyLabelInfo?.safetyLabel + + if (safetyLabelInfo == null || + safetyLabelInfo == UNAVAILABLE || + safetyLabel == null) { Log.e(LOG_TAG, "Safety label for $packageName not found") value = null return } - // TODO(b/260144598): link to app store + val installSourcePackageName = safetyLabelInfo.installSourcePackageName + val installSourceLabel: CharSequence? = + installSourcePackageName?.let { + KotlinUtils.getPackageLabel(app, it, Process.myUserHandle()) + } + value = PermissionRationaleInfo( permissionGroupName, - null, + installSourcePackageName, + installSourceLabel, getSafetyLabelSharingPurposesForGroup(safetyLabel, permissionGroupName)) } @@ -124,6 +142,15 @@ class PermissionRationaleViewModel( } } + fun canLinkToAppStore(context: Context, installSourcePackageName: String): Boolean { + return getAppStoreIntent(context, installSourcePackageName, packageName) != null + } + + fun sendToAppStore(context: Context, installSourcePackageName: String) { + val storeIntent = getAppStoreIntent(context, installSourcePackageName, packageName) + context.startActivity(storeIntent) + } + companion object { private val LOG_TAG = PermissionRationaleViewModel::class.java.simpleName } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java index b81c802da..819291d50 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/v34/PermissionRationaleActivity.java @@ -33,6 +33,9 @@ import android.icu.lang.UCharacter; import android.icu.text.ListFormatter; import android.os.Build; import android.os.Bundle; +import android.text.Annotation; +import android.text.SpannableStringBuilder; +import android.text.style.ClickableSpan; import android.util.Log; import android.view.MotionEvent; import android.view.View; @@ -57,6 +60,7 @@ import com.android.permissioncontroller.permission.utils.KotlinUtils; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; @@ -73,13 +77,34 @@ public class PermissionRationaleActivity extends SettingsActivity implements private static final String KEY_SESSION_ID = PermissionRationaleActivity.class.getName() + "_SESSION_ID"; + /** + * [Annotation] key for span annotations replacement within the permission rationale purposes + * string resource + */ + public static final String ANNOTATION_ID_KEY = "id"; + /** + * [Annotation] id value for span annotations replacement of link annotations within the + * permission rationale purposes string resource + */ + public static final String LINK_ANNOTATION_ID = "link"; + /** + * [Annotation] id value for span annotations replacement of install source annotations within + * the permission rationale purposes string resource + */ + public static final String INSTALL_SOURCE_ANNOTATION_ID = "install_source"; + /** + * [Annotation] id value for span annotations replacement of purpose list annotations within + * the permission rationale purposes string resource + */ + public static final String PURPOSE_LIST_ANNOTATION_ID = "purpose_list"; + /** Unique Id of a request. Inherited from GrantPermissionDialog if provide via intent extra */ private long mSessionId; /** Package that shall have permissions granted */ private String mTargetPackage; /** The permission group that initiated the permission rationale details activity */ private String mPermissionGroupName; - /** The permission rationale info resulting from the specified permisison and group */ + /** The permission rationale info resulting from the specified permission and group */ private PermissionRationaleInfo mPermissionRationaleInfo; private PermissionRationaleViewHandler mViewHandler; @@ -93,6 +118,14 @@ public class PermissionRationaleActivity extends SettingsActivity implements protected void onCreate(Bundle icicle) { super.onCreate(icicle); + if (!KotlinUtils.INSTANCE.isPermissionRationaleEnabled()) { + Log.e( + LOG_TAG, + "Permission rationale feature disabled"); + finishAfterTransition(); + return; + } + if (icicle == null) { mSessionId = getIntent().getLongExtra(Constants.EXTRA_SESSION_ID, new Random().nextLong()); @@ -235,9 +268,6 @@ public class PermissionRationaleActivity extends SettingsActivity implements } private void showPermissionRationale() { - String groupName = mPermissionRationaleInfo.getGroupName(); - String installSourceName = mPermissionRationaleInfo.getInstallSourceName(); - List<String> purposesList = new ArrayList<>(mPermissionRationaleInfo.getPurposeSet().size()); for (@Purpose int purpose : mPermissionRationaleInfo.getPurposeSet()) { @@ -247,20 +277,25 @@ public class PermissionRationaleActivity extends SettingsActivity implements // TODO(b/260144215): update purposes join based on l18n feedback String purposesString = ListFormatter.getInstance().format(purposesList); - // TODO(b/260144598): link to app store + String installSourcePackageName = mPermissionRationaleInfo.getInstallSourcePackageName(); + CharSequence installSourceLabel = mPermissionRationaleInfo.getInstallSourceLabel(); CharSequence purposeMessage; - if (installSourceName == null || installSourceName.isEmpty()) { - purposeMessage = - getString(R.string.permission_rationale_purpose_default_source_message, - purposesString); + if (installSourcePackageName == null || installSourcePackageName.length() == 0 + || installSourceLabel == null || installSourceLabel.length() == 0) { + purposeMessage = getString( + R.string.permission_rationale_purpose_default_source_message, + purposesString); } else { purposeMessage = - getString(R.string.permission_rationale_purpose_message, - installSourceName, - purposesString); + createPurposeMessageWithLink( + getText(R.string.permission_rationale_purpose_message), + installSourceLabel, + purposesString, + getLinkToAppStore(installSourcePackageName)); } // TODO(b/260144330): link to permission settings + String groupName = mPermissionRationaleInfo.getGroupName(); String permissionGroupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(this, groupName).toString(); CharSequence settingsMessage = @@ -306,4 +341,56 @@ public class PermissionRationaleActivity extends SettingsActivity implements throw new IllegalArgumentException("Invalid purpose: " + purpose); } } + + private CharSequence createPurposeMessageWithLink( + CharSequence purposeText, + CharSequence installSourceLabel, + CharSequence purposes, + ClickableSpan link) { + SpannableStringBuilder text = SpannableStringBuilder.valueOf(purposeText); + Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class); + // Sort the annotations in reverse order. + Arrays.sort(annotations, (a, b) -> text.getSpanStart(b) - text.getSpanStart(a)); + SpannableStringBuilder messageWithSpan = new SpannableStringBuilder(text); + for (android.text.Annotation annotation : annotations) { + if (!annotation.getKey().equals(ANNOTATION_ID_KEY)) { + continue; + } + + int spanStart = text.getSpanStart(annotation); + int spanEnd = text.getSpanEnd(annotation); + messageWithSpan.removeSpan(annotation); + + switch (annotation.getValue()) { + case INSTALL_SOURCE_ANNOTATION_ID: + messageWithSpan.replace(spanStart, spanEnd, installSourceLabel); + break; + case LINK_ANNOTATION_ID: + messageWithSpan.setSpan(link, spanStart, spanEnd, 0); + break; + case PURPOSE_LIST_ANNOTATION_ID: + messageWithSpan.replace(spanStart, spanEnd, purposes); + break; + default: + continue; + } + } + return messageWithSpan; + } + + private ClickableSpan getLinkToAppStore(String installSourcePackageName) { + boolean canLinkToAppStore = mViewModel + .canLinkToAppStore(PermissionRationaleActivity.this, installSourcePackageName); + if (!canLinkToAppStore) { + return null; + } + return new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + // TODO(b/259961958): metrics for click events + mViewModel.sendToAppStore(PermissionRationaleActivity.this, + installSourcePackageName); + } + }; + } } diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt index a7af4948d..fad90fc13 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt @@ -48,6 +48,7 @@ import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE import android.content.pm.PermissionGroupInfo import android.content.pm.PermissionInfo +import android.content.pm.ResolveInfo import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas @@ -1391,6 +1392,39 @@ object KotlinUtils { val permissions = HealthConnectManager.getHealthPermissions(context) PermissionMapping.addHealthPermissionsToPlatform(permissions) } + + /** + * Returns an [Intent] to the installer app store for a given package name, or {@code null} if + * none found + */ + fun getAppStoreIntent( + context: Context, + installerPackageName: String?, + packageName: String? + ): Intent? { + val intent: Intent = Intent(Intent.ACTION_SHOW_APP_INFO) + .setPackage(installerPackageName) + val result: Intent? = resolveActivityForIntent(context, intent) + if (result != null) { + result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) + return result + } + return null + } + + /** + * Verify that a component that supports the intent with action and return a new intent with + * same action and resolved class name set. Returns null if no activity resolution. + */ + private fun resolveActivityForIntent(context: Context, intent: Intent): Intent? { + val result: ResolveInfo? = context.packageManager.resolveActivity(intent, 0) + return if (result != null) { + Intent(intent.action) + .setClassName(result.activityInfo.packageName, result.activityInfo.name) + } else { + null + } + } } /** diff --git a/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java b/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java index 214b2db54..822e9c4fe 100644 --- a/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java +++ b/SafetyLabel/java/com/android/permission/safetylabel/SafetyLabel.java @@ -34,6 +34,7 @@ public class SafetyLabel { /** Returns {@link SafetyLabel} created by parsing a metadata {@link PersistableBundle} */ @Nullable public static SafetyLabel getSafetyLabelFromMetadata(@Nullable PersistableBundle bundle) { + // TODO(b/261069412): add versioning and nonnull empty/invalid cases if (bundle == null) { return null; } |