diff options
author | 2025-02-04 01:02:46 -0800 | |
---|---|---|
committer | 2025-02-04 16:44:38 -0800 | |
commit | 9b11f01bc5fa240369136053641558ac89502ed9 (patch) | |
tree | 9660053630eaafc5f60affce729cdf514ddcacd1 | |
parent | b25f034ab782e6bae168b1f8ee276206ef9fbdf8 (diff) |
Add recommended category for default app page
For assistant, it can be enabled by overlaying PermissionController
config_recommendedAssistants with a list of package names, optionally
with signing certificate digests if the app needs to be a non-system
app, similar to the config for default holders.
This is not available for the request role dialog yet and thus still
can't be used with a role that allows requesting.
Bug: 388234667
Relnote: N/A
Flag: com.android.permission.flags.default_apps_recommendation_enabled
Test: manual
Change-Id: I10a387c31362a126ee6264915f40f14926e06910
10 files changed, 223 insertions, 17 deletions
diff --git a/PermissionController/res/values/config.xml b/PermissionController/res/values/config.xml index 675368cba..657dc07dd 100644 --- a/PermissionController/res/values/config.xml +++ b/PermissionController/res/values/config.xml @@ -19,6 +19,15 @@ <bool name="config_showBrowserRole">true</bool> <bool name="config_showDialerRole">true</bool> <bool name="config_showSmsRole">true</bool> + <!-- + ~ Semicolon separated list of packages that are recommended for the assistant role. + ~ <p> + ~ This follows the same format as config_defaultAssistant and also requires a signing + ~ certificate digest (separated by a colon from the package name) if the app is not a system + ~ app. + --> + <string name="config_recommendedAssistants"></string> + <bool name="config_useAlternativePermGroupSummary">false</bool> <bool name="config_useWindowBlur">false</bool> <bool name="config_useMaterial3PermissionGrantDialog">false</bool> diff --git a/PermissionController/res/values/overlayable.xml b/PermissionController/res/values/overlayable.xml index e94c5bd04..72896fcdb 100644 --- a/PermissionController/res/values/overlayable.xml +++ b/PermissionController/res/values/overlayable.xml @@ -371,14 +371,15 @@ <item type="style" name="ThemeOverlay.PermissionSettings" /> <!-- END THEMES --> - <!-- START VISIBILITY CONFIGS --> - <!-- Assistant role uses: config_showDefaultAssistant --> - <!-- Home role uses: config_showDefaultHome --> - <!-- Emergency role uses: config_showDefaultEmergency --> + <!-- START ROLE CONFIGS --> + <!-- Assistant role uses android:bool/config_showDefaultAssistant --> + <!-- Home role uses android:bool/config_showDefaultHome --> + <!-- Emergency role uses android:bool/config_showDefaultEmergency --> <item type="bool" name="config_showBrowserRole" /> <item type="bool" name="config_showDialerRole" /> <item type="bool" name="config_showSmsRole" /> - <!-- END VISIBILITY CONFIGS --> + <item type="string" name="config_recommendedAssistants" /> + <!-- END ROLE CONFIGS --> <!-- START CAR DIMENS --> <item type="dimen" name="car_action_bar_height" /> diff --git a/PermissionController/res/values/strings.xml b/PermissionController/res/values/strings.xml index 48747bd2a..5714a2460 100644 --- a/PermissionController/res/values/strings.xml +++ b/PermissionController/res/values/strings.xml @@ -1337,6 +1337,12 @@ <!-- Title for category of default apps for private profile [CHAR LIMIT=50] --> <string name="default_apps_for_private_profile">Default for private space</string> + <!-- Title for category of apps that are optimized for the device [CHAR LIMIT=50] --> + <string name="default_app_recommended">Optimized for device</string> + + <!-- Title for category of other apps [CHAR LIMIT=50] --> + <string name="default_app_others">Others</string> + <!-- Summary of a default app when there is no app set [CHAR LIMIT=60] --> <string name="default_app_none">None</string> diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackage.java b/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackage.java index a3869b349..b13ac3731 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackage.java +++ b/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackage.java @@ -30,6 +30,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * A package name with an optional signing certificate. @@ -120,7 +121,7 @@ public class SignedPackage { return signedPackages; } - /* + /** * Checks whether this signed package is available, i.e. it is installed, and either has the * specified signing certificate or is a system app if no signing certificate is specified. * @@ -152,4 +153,36 @@ public class SignedPackage { } return true; } + + /** + * Checks whether an {@link ApplicationInfo} matches this signed package, i.e. it has the same + * package name, and either has the specified signing certificate or is a system app if no + * signing certificate is specified. + * + * @param applicationInfo the {@link ApplicationInfo} to check for + * @param context the {@code Context} to retrieve system services + * + * @return whether the {@link ApplicationInfo} matches this signed package + */ + public boolean matches(@NonNull ApplicationInfo applicationInfo, @NonNull Context context) { + if (!Objects.equals(applicationInfo.packageName, mPackageName)) { + return false; + } + if (mCertificate != null) { + UserHandle user = UserHandle.getUserHandleForUid(applicationInfo.uid); + if (!PackageUtils.hasSigningCertificateAsUser(mPackageName, mCertificate, user, + context)) { + Log.w(LOG_TAG, "Package doesn't have required signing certificate: " + + mPackageName); + return false; + } + } else { + if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + Log.w(LOG_TAG, "Package didn't specify a signing certificate and isn't a" + + " system app: " + mPackageName); + return false; + } + } + return true; + } } diff --git a/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackageUtils.java b/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackageUtils.java index 817c31c5f..169bed347 100644 --- a/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackageUtils.java +++ b/PermissionController/role-controller/java/com/android/role/controller/util/SignedPackageUtils.java @@ -17,6 +17,7 @@ package com.android.role.controller.util; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.UserHandle; @@ -81,4 +82,25 @@ public final class SignedPackageUtils { } return signedPackage.getPackageName(); } + + /** + * Check whether an {@link ApplicationInfo} matches any of the {@link SignedPackage}s. + * + * @param applicationInfo the {@link ApplicationInfo} to check for + * @param signedPackages the list of {@link SignedPackage}s to check against + * @param context the {@code Context} to retrieve system services + * + * @return whether the {@link ApplicationInfo} matches any of the {@link SignedPackage}s + */ + public static boolean matchesAny(@NonNull ApplicationInfo applicationInfo, + @NonNull List<SignedPackage> signedPackages, @NonNull Context context) { + int signedPackagesSize = signedPackages.size(); + for (int i = 0; i < signedPackagesSize; i++) { + SignedPackage signedPackage = signedPackages.get(i); + if (signedPackage.matches(applicationInfo, context)) { + return true; + } + } + return false; + } } diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppChildFragment.java b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppChildFragment.java index 5f870b1e3..268633c4f 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppChildFragment.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppChildFragment.java @@ -32,6 +32,7 @@ import androidx.appcompat.content.res.AppCompatResources; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceManager; @@ -39,6 +40,7 @@ import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; import com.android.modules.utils.build.SdkLevel; +import com.android.permission.flags.Flags; import com.android.permissioncontroller.R; import com.android.permissioncontroller.permission.utils.Utils; import com.android.permissioncontroller.role.utils.PackageUtils; @@ -63,6 +65,10 @@ public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat implements DefaultAppConfirmationDialogFragment.Listener, Preference.OnPreferenceClickListener { + private static final String PREFERENCE_KEY_RECOMMENDED_CATEGORY = + DefaultAppChildFragment.class.getName() + ".preference.RECOMMENDED_CATEGORY"; + private static final String PREFERENCE_KEY_OTHERS_CATEGORY = + DefaultAppChildFragment.class.getName() + ".preference.OTHERS_CATEGORY"; private static final String PREFERENCE_KEY_NONE = DefaultAppChildFragment.class.getName() + ".preference.NONE"; private static final String PREFERENCE_KEY_DESCRIPTION = DefaultAppChildFragment.class.getName() @@ -125,14 +131,21 @@ public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat ViewModelProvider.Factory viewModelFactory = new DefaultAppViewModel.Factory(mRole, mUser, activity.getApplication()); mViewModel = new ViewModelProvider(this, viewModelFactory).get(DefaultAppViewModel.class); + mViewModel.getRecommendedLiveData().observe(this, + applicationItems -> onApplicationListChanged()); mViewModel.getLiveData().observe(this, applicationItems -> onApplicationListChanged()); mViewModel.getManageRoleHolderStateLiveData().observe(this, this::onManageRoleHolderStateChanged); } private void onApplicationListChanged() { - List<RoleApplicationItem> applicationItems = mViewModel.getLiveData().getValue(); - if (applicationItems == null) { + List<RoleApplicationItem> recommendedApplicationItems = + mViewModel.getRecommendedLiveData().getValue(); + if (recommendedApplicationItems == null) { + return; + } + List<RoleApplicationItem> otherApplicationItems = mViewModel.getLiveData().getValue(); + if (otherApplicationItems == null) { return; } @@ -141,17 +154,43 @@ public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat Context context = preferenceManager.getContext(); PreferenceScreen preferenceScreen = preferenceFragment.getPreferenceScreen(); + PreferenceCategory oldRecommendedPreferenceCategory = null; + PreferenceCategory oldOthersPreferenceCategory = null; ArrayMap<String, Preference> oldPreferences = new ArrayMap<>(); if (preferenceScreen == null) { preferenceScreen = preferenceManager.createPreferenceScreen(context); preferenceFragment.setPreferenceScreen(preferenceScreen); } else { + if (Flags.defaultAppsRecommendationEnabled()) { + oldRecommendedPreferenceCategory = + preferenceScreen.findPreference(PREFERENCE_KEY_RECOMMENDED_CATEGORY); + clearPreferenceCategory(oldRecommendedPreferenceCategory, oldPreferences); + oldOthersPreferenceCategory = + preferenceScreen.findPreference(PREFERENCE_KEY_OTHERS_CATEGORY); + clearPreferenceCategory(oldOthersPreferenceCategory, oldPreferences); + } clearPreferences(preferenceScreen, oldPreferences); } - boolean noneChecked = !hasHolderApplication(applicationItems); - addNonePreferenceIfNeeded(preferenceScreen, noneChecked, oldPreferences, context); - addApplicationPreferences(preferenceScreen, applicationItems, oldPreferences, context); + if (Flags.defaultAppsRecommendationEnabled() && !recommendedApplicationItems.isEmpty()) { + addApplicationPreferenceCategory(oldRecommendedPreferenceCategory, + PREFERENCE_KEY_RECOMMENDED_CATEGORY, + getString(R.string.default_app_recommended), preferenceScreen, false, false, + recommendedApplicationItems, oldPreferences, context); + if (mRole.shouldShowNone() || !otherApplicationItems.isEmpty()) { + boolean noneChecked = !(hasHolderApplication(recommendedApplicationItems) + || hasHolderApplication(otherApplicationItems)); + addApplicationPreferenceCategory(oldOthersPreferenceCategory, + PREFERENCE_KEY_OTHERS_CATEGORY, getString(R.string.default_app_others), + preferenceScreen, true, noneChecked, otherApplicationItems, oldPreferences, + context); + } + } else { + boolean noneChecked = !hasHolderApplication(otherApplicationItems); + addNonePreferenceIfNeeded(preferenceScreen, noneChecked, oldPreferences, context); + addApplicationPreferences(preferenceScreen, otherApplicationItems, oldPreferences, + context); + } addNonPaymentNfcServicesPreference(preferenceScreen, oldPreferences, context); addDescriptionPreference(preferenceScreen, oldPreferences); @@ -159,6 +198,16 @@ public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat preferenceFragment.onPreferenceScreenChanged(); } + private static void clearPreferenceCategory(@Nullable PreferenceCategory preferenceCategory, + @NonNull ArrayMap<String, Preference> oldPreferences) { + if (preferenceCategory == null) { + return; + } + clearPreferences(preferenceCategory, oldPreferences); + preferenceCategory.getParent().removePreference(preferenceCategory); + preferenceCategory.setOrder(Preference.DEFAULT_ORDER); + } + private static void clearPreferences(@NonNull PreferenceGroup preferenceGroup, @NonNull ArrayMap<String, Preference> oldPreferences) { for (int i = preferenceGroup.getPreferenceCount() - 1; i >= 0; --i) { @@ -170,6 +219,25 @@ public class DefaultAppChildFragment<PF extends PreferenceFragmentCompat } } + private void addApplicationPreferenceCategory( + @Nullable PreferenceCategory oldPreferenceCategory, @NonNull String key, + @Nullable String title, @NonNull PreferenceScreen preferenceScreen, + boolean addNonePreferenceIfNeeded, boolean noneChecked, + @NonNull List<RoleApplicationItem> applicationItems, + @NonNull ArrayMap<String, Preference> oldPreferences, @NonNull Context context) { + PreferenceCategory preferenceCategory = oldPreferenceCategory; + if (preferenceCategory == null) { + preferenceCategory = new PreferenceCategory(context); + preferenceCategory.setKey(key); + preferenceCategory.setTitle(title); + } + preferenceScreen.addPreference(preferenceCategory); + if (addNonePreferenceIfNeeded) { + addNonePreferenceIfNeeded(preferenceCategory, noneChecked, oldPreferences, context); + } + addApplicationPreferences(preferenceCategory, applicationItems, oldPreferences, context); + } + private static boolean hasHolderApplication( @NonNull List<RoleApplicationItem> applicationItems) { int applicationItemsSize = applicationItems.size(); diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppViewModel.java b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppViewModel.java index 790c55d84..f4ba94f1d 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppViewModel.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/DefaultAppViewModel.java @@ -28,10 +28,12 @@ import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; +import com.android.permissioncontroller.role.utils.RoleUiBehaviorUtils; import com.android.permissioncontroller.role.utils.UserUtils; import com.android.role.controller.model.Role; import java.util.List; +import java.util.function.Predicate; /** * {@link ViewModel} for a default app. @@ -46,6 +48,9 @@ public class DefaultAppViewModel extends AndroidViewModel { private final UserHandle mUser; @NonNull + private final LiveData<List<RoleApplicationItem>> mRecommendedLiveData; + + @NonNull private final LiveData<List<RoleApplicationItem>> mLiveData; @NonNull @@ -61,22 +66,34 @@ public class DefaultAppViewModel extends AndroidViewModel { mUser = role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP ? UserUtils.getProfileParentOrSelf(user, application) : user; - RoleLiveData liveData = new RoleLiveData(role, mUser, application); + RoleLiveData userLiveData = new RoleLiveData(role, mUser, application); RoleSortFunction sortFunction = new RoleSortFunction(application); + LiveData<List<RoleApplicationItem>> liveData; if (role.getExclusivity() == Role.EXCLUSIVITY_PROFILE_GROUP) { // Context user might be work profile, ensure we get a non-null UserHandle if work // profile exists. getWorkProfile returns null if context user is work profile. UserHandle workProfile = UserUtils.getWorkProfileOrSelf(application); if (workProfile != null) { RoleLiveData workLiveData = new RoleLiveData(role, workProfile, application); - mLiveData = Transformations.map(new MergeRoleLiveData(liveData, workLiveData), + liveData = Transformations.map(new MergeRoleLiveData(userLiveData, workLiveData), sortFunction); } else { - mLiveData = Transformations.map(liveData, sortFunction); + liveData = Transformations.map(userLiveData, sortFunction); } } else { - mLiveData = Transformations.map(liveData, sortFunction); + liveData = Transformations.map(userLiveData, sortFunction); } + Predicate<RoleApplicationItem> recommendedApplicationFilter = + RoleUiBehaviorUtils.getRecommendedApplicationFilter(role, application); + mRecommendedLiveData = Transformations.map(liveData, + new ListLiveDataFilterFunction<>(recommendedApplicationFilter)); + mLiveData = Transformations.map(liveData, + new ListLiveDataFilterFunction<>(recommendedApplicationFilter.negate())); + } + + @NonNull + public LiveData<List<RoleApplicationItem>> getRecommendedLiveData() { + return mRecommendedLiveData; } @NonNull diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/AssistantRoleUiBehavior.java b/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/AssistantRoleUiBehavior.java index 4df3ccf99..c74c3d519 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/AssistantRoleUiBehavior.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/AssistantRoleUiBehavior.java @@ -25,8 +25,15 @@ import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.permission.flags.Flags; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.role.ui.RoleApplicationItem; import com.android.role.controller.model.Role; +import com.android.role.controller.util.SignedPackage; +import com.android.role.controller.util.SignedPackageUtils; + +import java.util.List; +import java.util.function.Predicate; /*** * Class for UI behavior of Assistant role @@ -39,14 +46,26 @@ public class AssistantRoleUiBehavior implements RoleUiBehavior { @NonNull Context context) { boolean isAutomotive = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); - if (isAutomotive) { return null; } - return new Intent(Settings.ACTION_VOICE_INPUT_SETTINGS); } + @NonNull + @Override + public Predicate<RoleApplicationItem> getRecommendedApplicationFilter( + @NonNull Role role, @NonNull Context context) { + if (Flags.defaultAppsRecommendationEnabled()) { + List<SignedPackage> signedPackages = SignedPackage.parseList( + context.getResources().getString(R.string.config_recommendedAssistants)); + return applicationItem -> SignedPackageUtils.matchesAny( + applicationItem.getApplicationInfo(), signedPackages, context); + } else { + return RoleUiBehavior.super.getRecommendedApplicationFilter(role, context); + } + } + @Nullable @Override public CharSequence getConfirmationMessage(@NonNull Role role, @NonNull String packageName, diff --git a/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/RoleUiBehavior.java b/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/RoleUiBehavior.java index ae5c03676..e1bf213a0 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/RoleUiBehavior.java +++ b/PermissionController/src/com/android/permissioncontroller/role/ui/behavior/RoleUiBehavior.java @@ -26,10 +26,12 @@ import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.permissioncontroller.role.ui.RequestRoleItemView; +import com.android.permissioncontroller.role.ui.RoleApplicationItem; import com.android.permissioncontroller.role.ui.TwoTargetPreference; import com.android.role.controller.model.Role; import java.util.List; +import java.util.function.Predicate; /*** * Interface for UI behavior for roles @@ -92,6 +94,20 @@ public interface RoleUiBehavior { @NonNull UserHandle user, @NonNull Context context) {} /** + * Get the filter for recommended applications of this role. + * + * @param role the role to get the recommended application filter for + * @param context the {@code Context} to retrieve system services + * + * @return the filter for recommended applications + */ + @NonNull + default Predicate<RoleApplicationItem> getRecommendedApplicationFilter( + @NonNull Role role, @NonNull Context context) { + return applicationItem -> false; + } + + /** * Get the confirmation message for adding an application as a holder of this role. * * @param role the role to get confirmation message for diff --git a/PermissionController/src/com/android/permissioncontroller/role/utils/RoleUiBehaviorUtils.java b/PermissionController/src/com/android/permissioncontroller/role/utils/RoleUiBehaviorUtils.java index c11a74259..255d88ff0 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/utils/RoleUiBehaviorUtils.java +++ b/PermissionController/src/com/android/permissioncontroller/role/utils/RoleUiBehaviorUtils.java @@ -26,12 +26,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.permissioncontroller.role.ui.RequestRoleItemView; +import com.android.permissioncontroller.role.ui.RoleApplicationItem; import com.android.permissioncontroller.role.ui.RoleApplicationPreference; import com.android.permissioncontroller.role.ui.RolePreference; import com.android.permissioncontroller.role.ui.behavior.RoleUiBehavior; import com.android.role.controller.model.Role; import java.util.List; +import java.util.function.Predicate; /** * Utility methods for Role UI behavior @@ -117,6 +119,19 @@ public final class RoleUiBehaviorUtils { } /** + * @see RoleUiBehavior#getRecommendedApplicationFilter + */ + @NonNull + public static Predicate<RoleApplicationItem> getRecommendedApplicationFilter( + @NonNull Role role, @NonNull Context context) { + RoleUiBehavior uiBehavior = getUiBehavior(role); + if (uiBehavior == null) { + return applicationItem -> false; + } + return uiBehavior.getRecommendedApplicationFilter(role, context); + } + + /** * @see RoleUiBehavior#getConfirmationMessage */ @Nullable |