diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a23eacaef2e..0be2e2efbac 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -481,7 +481,7 @@ + android:value="com.android.settings.wifi.savedaccesspoints2.SavedAccessPointsWifiSettings2" /> + android:textDirection="locale"/> - - Languages - - Preferred language order - - - System language - Remove @@ -352,13 +346,13 @@ Preferred Language - App languages + App Languages Set the language for each app - App language + App Language Suggested languages @@ -381,18 +375,6 @@ Only apps that support language selection are shown here. - - Your system, apps, and websites use the first supported language from your preferred languages. - - - To select a language for each app, go to app language settings. - - - Learn more about languages - - - https://support.google.com/android?p=per_language_app_settings - Remove selected language? @@ -408,7 +390,7 @@ Keep at least one preferred language - Not available as system language + May not be available in some apps Move up diff --git a/res/xml/languages.xml b/res/xml/languages.xml deleted file mode 100644 index ed65df099d0..00000000000 --- a/res/xml/languages.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java index 1c12c6a2ed2..c7e74ef4ac3 100644 --- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java @@ -182,9 +182,9 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title); title.setText(mCachedDevice.getName()); final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); - summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); if (!mCachedDevice.isConnected() || mCachedDevice.isBusy()) { + summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); updateDisconnectLayout(); return; } @@ -196,6 +196,9 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE); mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE); + summary.setText(mCachedDevice.getConnectionSummary( + BluetoothUtils.getIntMetaData(device, BluetoothDevice.METADATA_MAIN_BATTERY) + != BluetoothUtils.META_INT_ERROR)); updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle), BluetoothDevice.METADATA_MAIN_ICON, BluetoothDevice.METADATA_MAIN_BATTERY, @@ -207,6 +210,7 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET) || BluetoothUtils.getBooleanMetaData(device, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { + summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); updateSubLayout(mLayoutPreference.findViewById(R.id.layout_left), BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY, diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index b3c2e3071af..ab9110d01ab 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; + class LocaleDragAndDropAdapter extends RecyclerView.Adapter { @@ -153,10 +154,8 @@ class LocaleDragAndDropAdapter final LocaleDragCell dragCell = holder.getLocaleDragCell(); final String label = feedItem.getFullNameNative(); final String description = feedItem.getFullNameInUiLanguage(); - dragCell.setLabelAndDescription(label, description); dragCell.setLocalized(feedItem.isTranslated()); - dragCell.setCurrentDefault(feedItem.getLocale().equals(Locale.getDefault())); dragCell.setMiniLabel(mNumberFormatter.format(i + 1)); dragCell.setShowCheckbox(mRemoveMode); dragCell.setShowMiniLabel(!mRemoveMode); diff --git a/src/com/android/settings/localepicker/LocaleDragCell.java b/src/com/android/settings/localepicker/LocaleDragCell.java index 2f4cfefefb1..ea86189b018 100644 --- a/src/com/android/settings/localepicker/LocaleDragCell.java +++ b/src/com/android/settings/localepicker/LocaleDragCell.java @@ -33,7 +33,6 @@ class LocaleDragCell extends RelativeLayout { private CheckBox mCheckbox; private TextView mMiniLabel; private TextView mLocalized; - private TextView mCurrentDefault; private ImageView mDragHandle; public LocaleDragCell(Context context, AttributeSet attrs) { @@ -45,7 +44,6 @@ class LocaleDragCell extends RelativeLayout { super.onFinishInflate(); mLabel = (TextView) findViewById(R.id.label); mLocalized = (TextView) findViewById(R.id.l10nWarn); - mCurrentDefault = (TextView) findViewById(R.id.default_locale); mMiniLabel = (TextView) findViewById(R.id.miniLabel); mCheckbox = (CheckBox) findViewById(R.id.checkbox); mDragHandle = (ImageView) findViewById(R.id.dragHandle); @@ -102,14 +100,6 @@ class LocaleDragCell extends RelativeLayout { invalidate(); } - /** - * Indicate current locale is system default. - */ - public void setCurrentDefault(boolean isCurrentDefault) { - mCurrentDefault.setVisibility(isCurrentDefault ? VISIBLE : GONE); - invalidate(); - } - public ImageView getDragHandle() { return mDragHandle; } diff --git a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java b/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java deleted file mode 100644 index 05c740139cc..00000000000 --- a/src/com/android/settings/localepicker/LocaleHelperPreferenceController.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.settings.localepicker; - -import android.content.Context; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settingslib.HelpUtils; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.widget.FooterPreference; - -/** - * A controller to update current locale information of application. - */ -public class LocaleHelperPreferenceController extends AbstractPreferenceController { - private static final String TAG = LocaleHelperPreferenceController.class.getSimpleName(); - - private static final String KEY_FOOTER_LANGUAGE_PICKER = "footer_languages_picker"; - - public LocaleHelperPreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public String getPreferenceKey() { - return KEY_FOOTER_LANGUAGE_PICKER; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - FooterPreference footerPreference = screen.findPreference(getPreferenceKey()); - updateFooterPreference(footerPreference); - } - - @VisibleForTesting - void updateFooterPreference(FooterPreference footerPreference) { - if (footerPreference != null) { - footerPreference.setLearnMoreAction(v -> openLocaleLearnMoreLink()); - footerPreference.setLearnMoreText(mContext.getString( - R.string.desc_locale_helper_footer_general)); - } - } - - private void openLocaleLearnMoreLink() { - mContext.startActivity( - HelpUtils.getHelpIntent( - mContext, - mContext.getString(R.string.link_locale_picker_footer_learn_more), - /*backupContext=*/"")); - } -} diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index 9db3468c9e1..eac2dd1df3c 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -36,7 +36,6 @@ import android.widget.TextView; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; -import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.app.LocalePicker; @@ -47,7 +46,6 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.search.SearchIndexableRaw; -import com.android.settingslib.widget.LayoutPreference; import java.util.ArrayList; import java.util.List; @@ -66,7 +64,6 @@ public class LocaleListEditor extends RestrictedSettingsFragment { private static final int REQUEST_LOCALE_PICKER = 0; private static final String INDEX_KEY_ADD_LANGUAGE = "add_language"; - private static final String KEY_LANGUAGES_PICKER = "languages_picker"; private LocaleDragAndDropAdapter mAdapter; private Menu mMenu; @@ -75,9 +72,6 @@ public class LocaleListEditor extends RestrictedSettingsFragment { private boolean mShowingRemoveDialog; private boolean mIsUiRestricted; - private LayoutPreference mLocalePickerPreference; - private LocaleHelperPreferenceController mLocaleHelperPreferenceController; - public LocaleListEditor() { super(DISALLOW_CONFIG_LOCALE); } @@ -92,14 +86,6 @@ public class LocaleListEditor extends RestrictedSettingsFragment { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - addPreferencesFromResource(R.xml.languages); - final Activity activity = getActivity(); - activity.setTitle(R.string.language_picker_title); - mLocaleHelperPreferenceController = new LocaleHelperPreferenceController(activity); - final PreferenceScreen screen = getPreferenceScreen(); - mLocalePickerPreference = screen.findPreference(KEY_LANGUAGES_PICKER); - mLocaleHelperPreferenceController.displayPreference(screen); - LocaleStore.fillCache(this.getContext()); final List feedsList = getUserLocaleList(); mAdapter = new LocaleDragAndDropAdapter(this.getContext(), feedsList); @@ -107,8 +93,11 @@ public class LocaleListEditor extends RestrictedSettingsFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstState) { - configureDragAndDrop(mLocalePickerPreference); - return super.onCreateView(inflater, container, savedInstState); + final View result = super.onCreateView(inflater, container, savedInstState); + final View myLayout = inflater.inflate(R.layout.locale_order_list, (ViewGroup) result); + + configureDragAndDrop(myLayout); + return result; } @Override @@ -298,8 +287,8 @@ public class LocaleListEditor extends RestrictedSettingsFragment { return result; } - private void configureDragAndDrop(LayoutPreference layout) { - final RecyclerView list = layout.findViewById(R.id.dragList); + private void configureDragAndDrop(View view) { + final RecyclerView list = view.findViewById(R.id.dragList); final LocaleLinearLayoutManager llm = new LocaleLinearLayoutManager(getContext(), mAdapter); llm.setAutoMeasureEnabled(true); list.setLayoutManager(llm); @@ -308,7 +297,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment { mAdapter.setRecyclerView(list); list.setAdapter(mAdapter); - mAddLanguage = layout.findViewById(R.id.add_language); + mAddLanguage = view.findViewById(R.id.add_language); mAddLanguage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt index e1ee766603f..c5e84ae886d 100644 --- a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt +++ b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt @@ -17,19 +17,18 @@ package com.android.settings.spa.app.appsettings import android.app.ActivityManager -import android.app.admin.DevicePolicyManager +import android.content.ComponentName import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo -import android.os.UserManager -import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.Utils -import com.android.settingslib.spaprivileged.model.app.userId +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.model.app.isDisallowControl class AppButtonRepository(private val context: Context) { private val packageManager = context.packageManager - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val devicePolicyManager = context.devicePolicyManager /** * Checks whether the given application is disallowed from modifying. @@ -41,20 +40,10 @@ class AppButtonRepository(private val context: Context) { // If the uninstallation intent is already queued, disable the button. devicePolicyManager.isUninstallInQueue(app.packageName) -> true - RestrictedLockUtilsInternal.hasBaseUserRestriction( - context, UserManager.DISALLOW_APPS_CONTROL, app.userId - ) -> true - - else -> false + else -> app.isDisallowControl(context) } - /** - * Checks whether the given application is an active admin. - */ - fun isActiveAdmin(app: ApplicationInfo): Boolean = - devicePolicyManager.packageHasActiveAdmins(app.packageName, app.userId) - - fun getHomePackageInfo(): AppUninstallButton.HomePackages { + fun getHomePackageInfo(): HomePackages { val homePackages = mutableSetOf() val homeActivities = ArrayList() val currentDefaultHome = packageManager.getHomeActivities(homeActivities) @@ -66,7 +55,7 @@ class AppButtonRepository(private val context: Context) { homePackages.add(metaPackageName) } } - return AppUninstallButton.HomePackages(homePackages, currentDefaultHome) + return HomePackages(homePackages, currentDefaultHome) } private fun signaturesMatch(packageName1: String, packageName2: String): Boolean = try { @@ -75,4 +64,9 @@ class AppButtonRepository(private val context: Context) { // e.g. named alternate package not found during lookup; this is an expected case sometimes false } + + data class HomePackages( + val homePackages: Set, + val currentDefaultHome: ComponentName?, + ) } diff --git a/src/com/android/settings/spa/app/appsettings/AppButtons.kt b/src/com/android/settings/spa/app/appsettings/AppButtons.kt index 89f3b138df1..30fcdb77515 100644 --- a/src/com/android/settings/spa/app/appsettings/AppButtons.kt +++ b/src/com/android/settings/spa/app/appsettings/AppButtons.kt @@ -32,9 +32,11 @@ fun AppButtons(packageInfoPresenter: PackageInfoPresenter) { } private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPresenter) { - private val appLaunchButton = AppLaunchButton(context = packageInfoPresenter.context) + private val appLaunchButton = AppLaunchButton(packageInfoPresenter) + private val appInstallButton = AppInstallButton(packageInfoPresenter) private val appDisableButton = AppDisableButton(packageInfoPresenter) private val appUninstallButton = AppUninstallButton(packageInfoPresenter) + private val appClearButton = AppClearButton(packageInfoPresenter) private val appForceStopButton = AppForceStopButton(packageInfoPresenter) @Composable @@ -46,14 +48,17 @@ private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPres private fun getActionButtons(packageInfo: PackageInfo): List = listOfNotNull( appLaunchButton.getActionButton(packageInfo), + appInstallButton.getActionButton(packageInfo), appDisableButton.getActionButton(packageInfo), appUninstallButton.getActionButton(packageInfo), + appClearButton.getActionButton(packageInfo), appForceStopButton.getActionButton(packageInfo), ) @Composable fun Dialogs() { appDisableButton.DisableConfirmDialog() + appClearButton.ClearConfirmDialog() appForceStopButton.ForceStopConfirmDialog() } } diff --git a/src/com/android/settings/spa/app/appsettings/AppClearButton.kt b/src/com/android/settings/spa/app/appsettings/AppClearButton.kt new file mode 100644 index 00000000000..e16e09168d3 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppClearButton.kt @@ -0,0 +1,80 @@ +/* + * 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.settings.spa.app.appsettings + +import android.content.pm.PackageInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settingslib.spa.widget.button.ActionButton + +class AppClearButton( + private val packageInfoPresenter: PackageInfoPresenter, +) { + private val context = packageInfoPresenter.context + + private var openConfirmDialog by mutableStateOf(false) + + fun getActionButton(packageInfo: PackageInfo): ActionButton? { + val app = packageInfo.applicationInfo + if (!app.isInstantApp) return null + + return clearButton() + } + + private fun clearButton() = ActionButton( + text = context.getString(R.string.clear_instant_app_data), + imageVector = Icons.Outlined.Delete, + ) { openConfirmDialog = true } + + @Composable + fun ClearConfirmDialog() { + if (!openConfirmDialog) return + AlertDialog( + onDismissRequest = { openConfirmDialog = false }, + confirmButton = { + TextButton( + onClick = { + openConfirmDialog = false + packageInfoPresenter.clearInstantApp() + }, + ) { + Text(stringResource(R.string.clear_instant_app_data)) + } + }, + dismissButton = { + TextButton(onClick = { openConfirmDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + title = { + Text(stringResource(R.string.clear_instant_app_data)) + }, + text = { + Text(stringResource(R.string.clear_instant_app_confirmation)) + }, + ) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt index ff3fbe3c10e..cc5a76e2f99 100644 --- a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt @@ -16,10 +16,7 @@ package com.android.settings.spa.app.appsettings -import android.app.admin.DevicePolicyManager -import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowCircleDown import androidx.compose.material.icons.outlined.HideSource @@ -34,10 +31,12 @@ import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settings.Utils import com.android.settings.overlay.FeatureFactory -import com.android.settingslib.Utils as SettingsLibUtils import com.android.settingslib.spa.widget.button.ActionButton -import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.framework.common.userManager +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed +import com.android.settingslib.Utils as SettingsLibUtils class AppDisableButton( private val packageInfoPresenter: PackageInfoPresenter, @@ -46,8 +45,8 @@ class AppDisableButton( private val appButtonRepository = AppButtonRepository(context) private val resources = context.resources private val packageManager = context.packageManager - private val userManager = context.getSystemService(UserManager::class.java)!! - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val userManager = context.userManager + private val devicePolicyManager = context.devicePolicyManager private val applicationFeatureProvider = FeatureFactory.getFactory(context).getApplicationFeatureProvider(context) @@ -55,7 +54,7 @@ class AppDisableButton( fun getActionButton(packageInfo: PackageInfo): ActionButton? { val app = packageInfo.applicationInfo - if (!app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null + if (!app.isSystemApp) return null return when { app.enabled && !app.isDisabledUntilUsed -> { @@ -84,7 +83,7 @@ class AppDisableButton( SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false // If this is a device admin, it can't be disabled. - appButtonRepository.isActiveAdmin(app) -> false + app.isActiveAdmin(context) -> false // We don't allow disabling DO/PO on *any* users if it's a system app, because // "disabling" is actually "downgrade to the system version + disable", and "downgrade" diff --git a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt index 49191637677..c34eff027d5 100644 --- a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt @@ -36,6 +36,7 @@ import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.userId class AppForceStopButton( @@ -61,7 +62,7 @@ class AppForceStopButton( */ private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when { // User can't force stop device admin. - appButtonRepository.isActiveAdmin(app) -> false + app.isActiveAdmin(context) -> false appButtonRepository.isDisallowControl(app) -> false diff --git a/src/com/android/settings/spa/app/appsettings/AppInstallButton.kt b/src/com/android/settings/spa/app/appsettings/AppInstallButton.kt new file mode 100644 index 00000000000..94290e6a2ad --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppInstallButton.kt @@ -0,0 +1,46 @@ +/* + * 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.settings.spa.app.appsettings + +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FileDownload +import com.android.settings.R +import com.android.settings.applications.AppStoreUtil +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spaprivileged.model.app.userHandle + +class AppInstallButton(private val packageInfoPresenter: PackageInfoPresenter) { + private val context = packageInfoPresenter.context + + fun getActionButton(packageInfo: PackageInfo): ActionButton? { + val app = packageInfo.applicationInfo + if (!app.isInstantApp) return null + + return AppStoreUtil.getAppStoreLink(packageInfoPresenter.contextAsUser, app.packageName) + ?.let { intent -> installButton(intent, app) } + } + + private fun installButton(intent: Intent, app: ApplicationInfo) = ActionButton( + text = context.getString(R.string.install_text), + imageVector = Icons.Outlined.FileDownload, + ) { + context.startActivityAsUser(intent, app.userHandle) + } +} diff --git a/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt index a983b675938..9f81661b03b 100644 --- a/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt @@ -16,7 +16,6 @@ package com.android.settings.spa.app.appsettings -import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo @@ -26,11 +25,12 @@ import com.android.settings.R import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spaprivileged.model.app.userHandle -class AppLaunchButton(private val context: Context) { - private val packageManager = context.packageManager +class AppLaunchButton(packageInfoPresenter: PackageInfoPresenter) { + private val context = packageInfoPresenter.context + private val packageManagerAsUser = packageInfoPresenter.packageManagerAsUser fun getActionButton(packageInfo: PackageInfo): ActionButton? = - packageManager.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent -> + packageManagerAsUser.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent -> launchButton(intent, packageInfo.applicationInfo) } diff --git a/src/com/android/settings/spa/app/appsettings/AppSettings.kt b/src/com/android/settings/spa/app/appsettings/AppSettings.kt index 4c960e5a17a..615fa75d860 100644 --- a/src/com/android/settings/spa/app/appsettings/AppSettings.kt +++ b/src/com/android/settings/spa/app/appsettings/AppSettings.kt @@ -60,7 +60,7 @@ object AppSettingsProvider : SettingsPageProvider { PackageInfoPresenter(context, packageName, userId, coroutineScope) } AppSettings(packageInfoPresenter) - packageInfoPresenter.PageCloser() + packageInfoPresenter.PackageRemoveDetector() } @Composable @@ -77,7 +77,13 @@ object AppSettingsProvider : SettingsPageProvider { @Composable private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return - RegularScaffold(title = stringResource(R.string.application_info_label)) { + val app = packageInfo.applicationInfo + RegularScaffold( + title = stringResource(R.string.application_info_label), + actions = { + AppSettingsMoreOptions(packageInfoPresenter, app) + } + ) { val appInfoProvider = remember { AppInfoProvider(packageInfo) } appInfoProvider.AppInfo() @@ -85,7 +91,6 @@ private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) { AppButtons(packageInfoPresenter) Category(title = stringResource(R.string.advanced_apps)) { - val app = packageInfo.applicationInfo DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app) ModifySystemSettingsAppListProvider.InfoPageEntryItem(app) PictureInPictureListProvider.InfoPageEntryItem(app) diff --git a/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt new file mode 100644 index 00000000000..d17ff33bf39 --- /dev/null +++ b/src/com/android/settings/spa/app/appsettings/AppSettingsMoreOptions.kt @@ -0,0 +1,85 @@ +/* + * 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.settings.spa.app.appsettings + +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.Utils +import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager +import com.android.settingslib.spaprivileged.framework.common.userManager +import com.android.settingslib.spaprivileged.model.app.PackageManagers +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin +import com.android.settingslib.spaprivileged.model.app.isDisallowControl +import com.android.settingslib.spaprivileged.model.app.userId + +@Composable +fun AppSettingsMoreOptions(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) { + val context = LocalContext.current + // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear data + // on all users. We also don't allow uninstalling for all users if it's DO/PO for any user. + val isProfileOrDeviceOwner = remember(app) { + Utils.isProfileOrDeviceOwner( + context.userManager, context.devicePolicyManager, app.packageName + ) + } + if (isProfileOrDeviceOwner) return + val shownUninstallUpdates = remember(app) { isShowUninstallUpdates(context, app) } + val shownUninstallForAllUsers = remember(app) { isShowUninstallForAllUsers(context, app) } + if (!shownUninstallUpdates && !shownUninstallForAllUsers) return + MoreOptionsAction { onDismissRequest -> + if (shownUninstallUpdates) { + DropdownMenuItem( + text = { Text(stringResource(R.string.app_factory_reset)) }, + onClick = { + onDismissRequest() + packageInfoPresenter.startUninstallActivity(forAllUsers = false) + }, + ) + } + if (shownUninstallForAllUsers) { + DropdownMenuItem( + text = { Text(stringResource(R.string.uninstall_all_users_text)) }, + onClick = { + onDismissRequest() + packageInfoPresenter.startUninstallActivity(forAllUsers = true) + }, + ) + } + } +} + +private fun isShowUninstallUpdates(context: Context, app: ApplicationInfo): Boolean = + app.isUpdatedSystemApp && context.userManager.isUserAdmin(app.userId) && + !app.isDisallowControl(context) && + !context.resources.getBoolean(R.bool.config_disable_uninstall_update) + +private fun isShowUninstallForAllUsers(context: Context, app: ApplicationInfo): Boolean = + app.userId == 0 && !app.isSystemApp && !app.isInstantApp && !app.isActiveAdmin(context) && + isOtherUserHasInstallPackage(context, app) + +private fun isOtherUserHasInstallPackage(context: Context, app: ApplicationInfo): Boolean = + context.userManager.aliveUsers + .filter { it.id != app.userId } + .any { PackageManagers.isPackageInstalledAsUser(app.packageName, it.id) } diff --git a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt index 53b36b99d11..cdb2cd01e8f 100644 --- a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt +++ b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt @@ -16,7 +16,6 @@ package com.android.settings.spa.app.appsettings -import android.app.admin.DevicePolicyManager import android.app.settings.SettingsEnums import android.content.ComponentName import android.content.Intent @@ -24,7 +23,6 @@ import android.content.om.OverlayManager import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.net.Uri -import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import com.android.settings.R @@ -33,27 +31,27 @@ import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAd import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.model.app.hasFlag +import com.android.settingslib.spaprivileged.model.app.isActiveAdmin import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.model.app.userId class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) { private val context = packageInfoPresenter.context private val appButtonRepository = AppButtonRepository(context) - private val userManager = context.getSystemService(UserManager::class.java)!! private val overlayManager = context.getSystemService(OverlayManager::class.java)!! - private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!! + private val devicePolicyManager = context.devicePolicyManager fun getActionButton(packageInfo: PackageInfo): ActionButton? { val app = packageInfo.applicationInfo - if (app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null + if (app.isSystemApp || app.isInstantApp) return null return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app)) } /** Gets whether a package can be uninstalled. */ private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when { - // When we have multiple users, there is a separate menu to uninstall for all users. - !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && userManager.users.size >= 2 -> false + !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false // Not allow to uninstall DO/PO. Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.userId) -> false @@ -101,7 +99,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) ) { onUninstallClicked(app) } private fun onUninstallClicked(app: ApplicationInfo) { - if (appButtonRepository.isActiveAdmin(app)) { + if (app.isActiveAdmin(context)) { packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN) val intent = Intent(context, DeviceAdminAdd::class.java).apply { putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, app.packageName) @@ -115,20 +113,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin) return } - startUninstallActivity(app) - } - - data class HomePackages( - val homePackages: Set, - val currentDefaultHome: ComponentName?, - ) - - private fun startUninstallActivity(app: ApplicationInfo) { - val packageUri = Uri.parse("package:${app.packageName}") - packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) - val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply { - putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, !app.hasFlag(ApplicationInfo.FLAG_INSTALLED)) - } - context.startActivityAsUser(intent, app.userHandle) + packageInfoPresenter.startUninstallActivity() } } diff --git a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt index eb39c4ff636..299c0a7dea1 100644 --- a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt +++ b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.net.Uri import android.os.UserHandle import android.util.Log import androidx.compose.runtime.Composable @@ -48,9 +49,8 @@ class PackageInfoPresenter( private val coroutineScope: CoroutineScope, ) { private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider - private val packageManager by lazy { - context.createContextAsUser(UserHandle.of(userId), 0).packageManager - } + val contextAsUser by lazy { context.createContextAsUser(UserHandle.of(userId), 0) } + val packageManagerAsUser: PackageManager by lazy { contextAsUser.packageManager } private val _flow: MutableStateFlow = MutableStateFlow(null) val flow: StateFlow = _flow @@ -66,17 +66,23 @@ class PackageInfoPresenter( } /** - * Closes the page when the package is uninstalled. + * Detects the package removed event. */ @Composable - fun PageCloser() { + fun PackageRemoveDetector() { val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply { addDataScheme("package") } val navController = LocalNavController.current DisposableBroadcastReceiverAsUser(userId, intentFilter) { intent -> if (packageName == intent.data?.schemeSpecificPart) { - navController.navigateBack() + val packageInfo = flow.value + if (packageInfo != null && packageInfo.applicationInfo.isSystemApp) { + // System app still exists after uninstalling the updates, refresh the page. + notifyChange() + } else { + navController.navigateBack() + } } } } @@ -85,7 +91,7 @@ class PackageInfoPresenter( fun enable() { logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) coroutineScope.launch(Dispatchers.IO) { - packageManager.setApplicationEnabledSetting( + packageManagerAsUser.setApplicationEnabledSetting( packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0 ) notifyChange() @@ -96,13 +102,32 @@ class PackageInfoPresenter( fun disable() { logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) coroutineScope.launch(Dispatchers.IO) { - packageManager.setApplicationEnabledSetting( + packageManagerAsUser.setApplicationEnabledSetting( packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 ) notifyChange() } } + /** Starts the uninstallation activity. */ + fun startUninstallActivity(forAllUsers: Boolean = false) { + logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) + val packageUri = Uri.parse("package:${packageName}") + val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply { + putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, forAllUsers) + } + context.startActivityAsUser(intent, UserHandle.of(userId)) + } + + /** Clears this instant app. */ + fun clearInstantApp() { + logAction(SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP) + coroutineScope.launch(Dispatchers.IO) { + packageManagerAsUser.deletePackageAsUser(packageName, null, 0, userId) + notifyChange() + } + } + /** Force stops this package. */ fun forceStop() { logAction(SettingsEnums.ACTION_APP_FORCE_STOP) diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java deleted file mode 100644 index 31b8e794480..00000000000 --- a/tests/unit/src/com/android/settings/localepicker/LocaleHelperPreferenceControllerTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.settings.localepicker; - -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.os.Looper; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.settingslib.widget.FooterPreference; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -public class LocaleHelperPreferenceControllerTest { - private Context mContext; - private LocaleHelperPreferenceController mLocaleHelperPreferenceController; - - @Mock - private FooterPreference mMockFooterPreference; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mContext = ApplicationProvider.getApplicationContext(); - mLocaleHelperPreferenceController = new LocaleHelperPreferenceController(mContext); - } - - @Test - public void updateFooterPreference_setFooterPreference_hasClickAction() { - mLocaleHelperPreferenceController.updateFooterPreference(mMockFooterPreference); - verify(mMockFooterPreference).setLearnMoreText(anyString()); - } -}