diff --git a/AndroidManifest.xml b/AndroidManifest.xml index aed51f3e0eb..5ffbbf84d01 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -153,6 +153,7 @@ android:requiredForAllUsers="true" android:supportsRtl="true" android:backupAgent="com.android.settings.backup.SettingsBackupHelper" + android:restoreAnyVersion="true" android:usesCleartextTraffic="true" android:defaultToDeviceProtectedStorage="true" android:directBootAware="true" diff --git a/res/values/strings.xml b/res/values/strings.xml index 2037323c8d4..dd51f522b1f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -150,14 +150,14 @@ Pair right ear Pair left ear - - For all available hearing devices - - More hearing device settings - - Change cross-device settings like shortcut, and telecoil controls - - For this device + + Hearing device settings + + Shortcut, hearing aid compatibility + + Presets + + Couldn\u2019t update preset Audio output @@ -1847,11 +1847,6 @@ Select maximum number of connected Bluetooth audio devices - - NFC stack debug log - - Increase NFC stack logging level - NFC verbose vendor debug log diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index d260554f937..91f73a70b6e 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -69,7 +69,7 @@ android:key="device_companion_apps"/> + android:key="hearing_device_group" /> diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index b1ebbb82ed5..23eb1f25241 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -469,11 +469,6 @@ android:entries="@array/bluetooth_max_connected_audio_devices" android:entryValues="@array/bluetooth_max_connected_audio_devices_values" /> - - profile instanceof HapClientProfile); + } + + @Override + public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetSelected, device: " + device.getAddress() + + ", presetIndex: " + presetIndex + ", reason: " + reason); + } + mContext.getMainExecutor().execute(this::refresh); + } + } + + @Override + public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, + "onPresetSelectionFailed, device: " + device.getAddress() + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @Override + public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @Override + public void onPresetInfoChanged(@NonNull BluetoothDevice device, + @NonNull List presetInfoList, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress() + + ", reason: " + reason + + ", infoList: " + presetInfoList); + } + mContext.getMainExecutor().execute(this::refresh); + } + } + + @Override + public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, + "onSetPresetNameFailed, device: " + device.getAddress() + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @Override + public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + private ListPreference createPresetPreference(Context context) { + ListPreference preference = new ListPreference(context); + preference.setKey(KEY_HEARING_AIDS_PRESETS); + preference.setOrder(ORDER_HEARING_AIDS_PRESETS); + preference.setTitle(context.getString(R.string.bluetooth_hearing_aids_presets)); + preference.setOnPreferenceChangeListener(this); + return preference; + } + + private void loadAllPresetInfo() { + if (mPreference == null) { + return; + } + List infoList = mHapClientProfile.getAllPresetInfo( + mCachedDevice.getDevice()); + CharSequence[] presetNames = new CharSequence[infoList.size()]; + CharSequence[] presetIndexes = new CharSequence[infoList.size()]; + for (int i = 0; i < infoList.size(); i++) { + presetNames[i] = infoList.get(i).getName(); + presetIndexes[i] = Integer.toString(infoList.get(i).getIndex()); + } + mPreference.setEntries(presetNames); + mPreference.setEntryValues(presetIndexes); + } + + @VisibleForTesting + @Nullable + ListPreference getPreference() { + return mPreference; + } + + void showErrorToast() { + Toast.makeText(mContext, R.string.bluetooth_hearing_aids_presets_error, + Toast.LENGTH_SHORT).show(); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java new file mode 100644 index 00000000000..3703b7180af --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.accessibility.Flags; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * The controller of the hearing device controls. + * + *

Note: It is responsible for creating the sub-controllers inside this preference + * category controller. + */ +public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsController { + + public static final int ORDER_HEARING_DEVICE_SETTINGS = 1; + public static final int ORDER_HEARING_AIDS_PRESETS = 2; + static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; + + private final List mControllers = new ArrayList<>(); + private Lifecycle mLifecycle; + private LocalBluetoothManager mManager; + + public BluetoothDetailsHearingDeviceController(@NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull LocalBluetoothManager manager, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mManager = manager; + mLifecycle = lifecycle; + } + + @VisibleForTesting + void setSubControllers( + BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController, + BluetoothDetailsHearingAidsPresetsController presetsController) { + mControllers.clear(); + mControllers.add(hearingDeviceSettingsController); + mControllers.add(presetsController); + } + + @Override + public boolean isAvailable() { + return mControllers.stream().anyMatch(BluetoothDetailsController::isAvailable); + } + + @Override + @NonNull + public String getPreferenceKey() { + return KEY_HEARING_DEVICE_GROUP; + } + + @Override + protected void init(PreferenceScreen screen) { + + } + + @Override + protected void refresh() { + + } + + /** + * Initiates the sub controllers controlled by this group controller. + * + *

Note: The caller must call this method when creating this class. + * + * @param isLaunchFromHearingDevicePage a boolean that determines if the caller is launch from + * hearing device page + */ + void initSubControllers(boolean isLaunchFromHearingDevicePage) { + mControllers.clear(); + // Don't need to show the entrance to hearing device page when launched from the same page + if (!isLaunchFromHearingDevicePage) { + mControllers.add(new BluetoothDetailsHearingDeviceSettingsController(mContext, + mFragment, mCachedDevice, mLifecycle)); + } + if (Flags.enableHearingAidPresetControl()) { + mControllers.add(new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, + mManager, mCachedDevice, mLifecycle)); + } + } + + @NonNull + public List getSubControllers() { + return mControllers; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java similarity index 71% rename from src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java rename to src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java index 162abc78aef..7e5f3b1a78f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java @@ -16,6 +16,9 @@ package com.android.settings.bluetooth; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_HEARING_DEVICE_SETTINGS; + import android.content.Context; import android.text.TextUtils; @@ -36,15 +39,13 @@ import com.google.common.annotations.VisibleForTesting; /** * The controller of the hearing device settings to launch Hearing device page. */ -public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDetailsController +public class BluetoothDetailsHearingDeviceSettingsController extends BluetoothDetailsController implements Preference.OnPreferenceClickListener { @VisibleForTesting - static final String KEY_DEVICE_CONTROLS_GENERAL_GROUP = "device_controls_general"; - @VisibleForTesting - static final String KEY_HEARING_DEVICE_CONTROLS = "hearing_device_controls"; + static final String KEY_HEARING_DEVICE_SETTINGS = "hearing_device_settings"; - public BluetoothDetailsHearingDeviceControlsController(Context context, + public BluetoothDetailsHearingDeviceSettingsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context, fragment, device, lifecycle); lifecycle.addObserver(this); @@ -57,37 +58,40 @@ public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDe @Override protected void init(PreferenceScreen screen) { - if (!mCachedDevice.isHearingAidDevice()) { + if (!isAvailable()) { return; } - - final PreferenceCategory prefCategory = screen.findPreference(getPreferenceKey()); - final Preference pref = createHearingDeviceControlsPreference(prefCategory.getContext()); - prefCategory.addPreference(pref); + final PreferenceCategory group = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + final Preference pref = createHearingDeviceSettingsPreference(group.getContext()); + group.addPreference(pref); } @Override - protected void refresh() {} + protected void refresh() { + + } @Override public String getPreferenceKey() { - return KEY_DEVICE_CONTROLS_GENERAL_GROUP; + return KEY_HEARING_DEVICE_SETTINGS; } @Override public boolean onPreferenceClick(Preference preference) { - if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_CONTROLS)) { + if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_SETTINGS)) { launchAccessibilityHearingDeviceSettings(); return true; } return false; } - private Preference createHearingDeviceControlsPreference(Context context) { + private Preference createHearingDeviceSettingsPreference(Context context) { final ArrowPreference preference = new ArrowPreference(context); - preference.setKey(KEY_HEARING_DEVICE_CONTROLS); - preference.setTitle(context.getString(R.string.bluetooth_device_controls_title)); - preference.setSummary(context.getString(R.string.bluetooth_device_controls_summary)); + preference.setKey(KEY_HEARING_DEVICE_SETTINGS); + preference.setOrder(ORDER_HEARING_DEVICE_SETTINGS); + preference.setTitle(context.getString(R.string.bluetooth_hearing_device_settings_title)); + preference.setSummary( + context.getString(R.string.bluetooth_hearing_device_settings_summary)); preference.setOnPreferenceClickListener(this); return preference; diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 9c68c9cc870..87b2c6b65d0 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -326,16 +326,16 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice, lifecycle)); - // Don't need to show hearing device again when launched from the same page. - if (!isLaunchFromHearingDevicePage()) { - controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this, - mCachedDevice, lifecycle)); - } - controllers.add(new BluetoothDetailsDataSyncController(context, this, - mCachedDevice, lifecycle)); - controllers.add( - new BluetoothDetailsExtraOptionsController( - context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsDataSyncController(context, this, mCachedDevice, + lifecycle)); + controllers.add(new BluetoothDetailsExtraOptionsController(context, this, mCachedDevice, + lifecycle)); + BluetoothDetailsHearingDeviceController hearingDeviceController = + new BluetoothDetailsHearingDeviceController(context, this, mManager, + mCachedDevice, lifecycle); + controllers.add(hearingDeviceController); + hearingDeviceController.initSubControllers(isLaunchFromHearingDevicePage()); + controllers.addAll(hearingDeviceController.getSubControllers()); } return controllers; } diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index 6ef5aa82a49..42e6d9c4b68 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -52,7 +52,6 @@ import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.Instrumentable; -import com.android.settingslib.datastore.ChangeReason; import com.android.settingslib.widget.LayoutPreference; import java.util.ArrayList; @@ -272,7 +271,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment public void onPause() { super.onPause(); - notifyBackupManager(); final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode(); mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode); logMetricCategory(currentOptimizeMode); @@ -289,13 +287,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment Log.d(TAG, "Leave with mode: " + currentOptimizeMode); } - @VisibleForTesting - void notifyBackupManager() { - if (mOptimizationMode != mBatteryOptimizeUtils.getAppOptimizationMode()) { - BatterySettingsStorage.get(getContext()).notifyChange(ChangeReason.UPDATE); - } - } - @VisibleForTesting void initHeader() { final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header); diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java index dc4aade4545..001876c394b 100644 --- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java @@ -33,6 +33,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action; +import com.android.settingslib.datastore.ChangeReason; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; import java.lang.annotation.Retention; @@ -222,6 +223,10 @@ public class BatteryOptimizeUtils { return; } + // App preferences are already clear when code reach here, and there may be no + // setAppUsageStateInternal call to notifyChange. So always trigger notifyChange here. + BatterySettingsStorage.get(context).notifyChange(ChangeReason.DELETE); + allowlistBackend.refreshList(); // Resets optimization mode for each application. for (ApplicationInfo info : applications) { @@ -351,6 +356,9 @@ public class BatteryOptimizeUtils { } BatteryOptimizeLogUtils.writeLog( context, action, packageNameKey, createLogEvent(appStandbyMode, allowListed)); + if (action != Action.RESET) { // reset has been notified in resetAppOptimizationMode + BatterySettingsStorage.get(context).notifyChange(toChangeReason(action)); + } } private static String createLogEvent(int appStandbyMode, boolean allowListed) { @@ -362,4 +370,8 @@ public class BatteryOptimizeUtils { allowListed, getAppOptimizationMode(appStandbyMode, allowListed)); } + + private static @ChangeReason int toChangeReason(Action action) { + return action == Action.RESTORE ? ChangeReason.RESTORE : ChangeReason.UPDATE; + } } diff --git a/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java index 56702cf5c2a..b662d3ef908 100644 --- a/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java +++ b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java @@ -41,7 +41,6 @@ import com.android.settingslib.HelpUtils; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.datastore.ChangeReason; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.widget.MainSwitchPreference; @@ -116,7 +115,6 @@ public class PowerBackgroundUsageDetail extends DashboardFragment public void onPause() { super.onPause(); - notifyBackupManager(); final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode(); mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode); logMetricCategory(currentOptimizeMode); @@ -183,13 +181,6 @@ public class PowerBackgroundUsageDetail extends DashboardFragment onRadioButtonClicked(isEnabled ? mOptimizePreference : null); } - @VisibleForTesting - void notifyBackupManager() { - if (mOptimizationMode != mBatteryOptimizeUtils.getAppOptimizationMode()) { - BatterySettingsStorage.get(getContext()).notifyChange(ChangeReason.UPDATE); - } - } - @VisibleForTesting int getSelectedPreference() { if (!mMainSwitchPreference.isChecked()) { diff --git a/src/com/android/settings/inputmethod/TrackpadTapDraggingPreferenceController.java b/src/com/android/settings/inputmethod/TrackpadTapDraggingPreferenceController.java index 28c2915e4d7..30253a8a30f 100644 --- a/src/com/android/settings/inputmethod/TrackpadTapDraggingPreferenceController.java +++ b/src/com/android/settings/inputmethod/TrackpadTapDraggingPreferenceController.java @@ -16,16 +16,22 @@ package com.android.settings.inputmethod; +import android.app.settings.SettingsEnums; import android.content.Context; import android.hardware.input.InputSettings; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class TrackpadTapDraggingPreferenceController extends TogglePreferenceController { + private MetricsFeatureProvider mMetricsFeatureProvider; + public TrackpadTapDraggingPreferenceController(Context context, String key) { super(context, key); + mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } @Override @@ -36,7 +42,8 @@ public class TrackpadTapDraggingPreferenceController extends TogglePreferenceCon @Override public boolean setChecked(boolean isChecked) { InputSettings.setTouchpadTapDragging(mContext, isChecked); - // TODO(b/321978150): add a metric for tap dragging settings changes. + mMetricsFeatureProvider.action( + mContext, SettingsEnums.ACTION_GESTURE_TAP_DRAGGING_CHANGED, isChecked); return true; } diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt index a0c363a206b..5a2a3947621 100644 --- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -44,13 +44,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settings.R import com.android.settings.network.SubscriptionInfoListViewModel import com.android.settings.network.telephony.MobileNetworkUtils +import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo import com.android.settings.wifi.WifiPickerTrackerHelper import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle -import com.android.settingslib.spa.widget.preference.ListPreferenceOption import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference @@ -173,27 +173,23 @@ fun PageImpl( ) { val selectableSubscriptionInfoList by selectableSubscriptionInfoListFlow .collectAsStateWithLifecycle(initialValue = emptyList()) - val activeSubscriptionInfoList: List = - selectableSubscriptionInfoList.filter { subscriptionInfo -> - subscriptionInfo.simSlotIndex != -1 - } val stringSims = stringResource(R.string.provider_network_settings_title) RegularScaffold(title = stringSims) { SimsSection(selectableSubscriptionInfoList) PrimarySimSectionImpl( - activeSubscriptionInfoList, - defaultVoiceSubId, - defaultSmsSubId, - defaultDataSubId, - nonDds + selectableSubscriptionInfoListFlow, + defaultVoiceSubId, + defaultSmsSubId, + defaultDataSubId, + nonDds ) } } @Composable fun PrimarySimImpl( - subscriptionInfoList: List, + primarySimInfo: PrimarySimInfo, callsSelectedId: MutableIntState, textsSelectedId: MutableIntState, mobileDataSelectedId: MutableIntState, @@ -237,108 +233,83 @@ fun PrimarySimImpl( } }, ) { - var state = rememberSaveable { mutableStateOf(false) } - var callsAndSmsList = remember { - mutableListOf(ListPreferenceOption(id = -1, text = "Loading")) - } - var dataList = remember { - mutableListOf(ListPreferenceOption(id = -1, text = "Loading")) + val telephonyManagerForNonDds: TelephonyManager? = + context.getSystemService(TelephonyManager::class.java) + ?.createForSubscriptionId(nonDds.intValue) + val automaticDataChecked = rememberSaveable() { + mutableStateOf(false) } - if (subscriptionInfoList.size >= 2) { - state.value = true - callsAndSmsList.clear() - dataList.clear() - for (info in subscriptionInfoList) { - var item = ListPreferenceOption( - id = info.subscriptionId, - text = "${info.displayName}", - summary = "${info.number}" - ) - callsAndSmsList.add(item) - dataList.add(item) - } - callsAndSmsList.add( - ListPreferenceOption( - id = SubscriptionManager.INVALID_SUBSCRIPTION_ID, - text = stringResource(id = R.string.sim_calls_ask_first_prefs_title) - ) - ) - } else { - // hide the primary sim - state.value = false - Log.d(NetworkCellularGroupProvider.name, "Hide primary sim") - } + CreatePrimarySimListPreference( + stringResource(id = R.string.primary_sim_calls_title), + primarySimInfo.callsAndSmsList, + callsSelectedId, + ImageVector.vectorResource(R.drawable.ic_phone), + actionSetCalls + ) + CreatePrimarySimListPreference( + stringResource(id = R.string.primary_sim_texts_title), + primarySimInfo.callsAndSmsList, + textsSelectedId, + Icons.AutoMirrored.Outlined.Message, + actionSetTexts + ) + CreatePrimarySimListPreference( + stringResource(id = R.string.mobile_data_settings_title), + primarySimInfo.dataList, + mobileDataSelectedId, + Icons.Outlined.DataUsage, + actionSetMobileData + ) - if (state.value) { - val telephonyManagerForNonDds: TelephonyManager? = - context.getSystemService(TelephonyManager::class.java) - ?.createForSubscriptionId(nonDds.intValue) - val automaticDataChecked = rememberSaveable() { - mutableStateOf(false) - } - - CreatePrimarySimListPreference( - stringResource(id = R.string.primary_sim_calls_title), - callsAndSmsList, - callsSelectedId, - ImageVector.vectorResource(R.drawable.ic_phone), - actionSetCalls - ) - CreatePrimarySimListPreference( - stringResource(id = R.string.primary_sim_texts_title), - callsAndSmsList, - textsSelectedId, - Icons.AutoMirrored.Outlined.Message, - actionSetTexts - ) - CreatePrimarySimListPreference( - stringResource(id = R.string.mobile_data_settings_title), - dataList, - mobileDataSelectedId, - Icons.Outlined.DataUsage, - actionSetMobileData - ) - - val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title) - val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg) - SwitchPreference( - object : SwitchPreferenceModel { - override val title = autoDataTitle - override val summary = { autoDataSummary } - override val checked = { - if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - coroutineScope.launch { - automaticDataChecked.value = getAutomaticData(telephonyManagerForNonDds) - Log.d( - NetworkCellularGroupProvider.name, - "NonDds:${nonDds.intValue}" + - "getAutomaticData:${automaticDataChecked.value}" - ) - } + val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title) + val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg) + SwitchPreference( + object : SwitchPreferenceModel { + override val title = autoDataTitle + override val summary = { autoDataSummary } + override val checked = { + if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + coroutineScope.launch { + automaticDataChecked.value = getAutomaticData(telephonyManagerForNonDds) + Log.d( + NetworkCellularGroupProvider.name, + "NonDds:${nonDds.intValue}" + + "getAutomaticData:${automaticDataChecked.value}" + ) } - automaticDataChecked.value - } - override val onCheckedChange: ((Boolean) -> Unit)? = { - automaticDataChecked.value = it - actionSetAutoDataSwitch(it) } + automaticDataChecked.value } - ) - } + override val onCheckedChange: ((Boolean) -> Unit)? = { + automaticDataChecked.value = it + actionSetAutoDataSwitch(it) + } + } + ) } @Composable fun PrimarySimSectionImpl( - subscriptionInfoList: List, + subscriptionInfoListFlow: Flow>, callsSelectedId: MutableIntState, textsSelectedId: MutableIntState, mobileDataSelectedId: MutableIntState, nonDds: MutableIntState, ) { + val context = LocalContext.current + val primarySimInfo = remember(subscriptionInfoListFlow) { + subscriptionInfoListFlow + .map { subscriptionInfoList -> + subscriptionInfoList.filter { subInfo -> subInfo.simSlotIndex != -1 } + } + .map(PrimarySimRepository(context)::getPrimarySimInfo) + .flowOn(Dispatchers.Default) + }.collectAsStateWithLifecycle(initialValue = null).value ?: return + Category(title = stringResource(id = R.string.primary_sim_title)) { PrimarySimImpl( - subscriptionInfoList, + primarySimInfo, callsSelectedId, textsSelectedId, mobileDataSelectedId, diff --git a/src/com/android/settings/spa/network/PrimarySimRepository.kt b/src/com/android/settings/spa/network/PrimarySimRepository.kt new file mode 100644 index 00000000000..b9eb3ffcb0a --- /dev/null +++ b/src/com/android/settings/spa/network/PrimarySimRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.network + +import android.content.Context +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.util.Log +import com.android.settings.R +import com.android.settings.network.SubscriptionUtil +import com.android.settingslib.spa.widget.preference.ListPreferenceOption + +class PrimarySimRepository(private val context: Context) { + + data class PrimarySimInfo( + val callsAndSmsList: List, + val dataList: List, + ) + + fun getPrimarySimInfo(selectableSubscriptionInfoList: List): PrimarySimInfo? { + if (selectableSubscriptionInfoList.size < 2) { + Log.d(TAG, "Hide primary sim") + return null + } + + val callsAndSmsList = mutableListOf() + val dataList = mutableListOf() + for (info in selectableSubscriptionInfoList) { + val item = ListPreferenceOption( + id = info.subscriptionId, + text = "${info.displayName}", + summary = SubscriptionUtil.getFormattedPhoneNumber(context, info) ?: "", + ) + callsAndSmsList += item + dataList += item + } + callsAndSmsList += ListPreferenceOption( + id = SubscriptionManager.INVALID_SUBSCRIPTION_ID, + text = context.getString(R.string.sim_calls_ask_first_prefs_title), + ) + + return PrimarySimInfo(callsAndSmsList, dataList) + } + + private companion object { + private const val TAG = "PrimarySimRepository" + } +} diff --git a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt index b9849666e53..a8c0575e3e8 100644 --- a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt +++ b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt @@ -24,10 +24,13 @@ import androidx.compose.material.icons.outlined.SignalCellularAlt import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.network.SimOnboardingService import com.android.settingslib.spa.framework.theme.SettingsDimension @@ -38,6 +41,9 @@ import com.android.settingslib.spa.widget.scaffold.BottomAppBarButton import com.android.settingslib.spa.widget.scaffold.SuwScaffold import com.android.settingslib.spa.widget.ui.SettingsBody import com.android.settingslib.spa.widget.ui.SettingsIcon +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn /** * the sim onboarding primary sim compose @@ -77,13 +83,19 @@ fun SimOnboardingPrimarySimImpl( SettingsBody(stringResource(id = R.string.sim_onboarding_primary_sim_msg)) } - var selectedSubscriptionInfoList = - onboardingService.getSelectedSubscriptionInfoListWithRenaming() + val context = LocalContext.current + val primarySimInfo = remember { + flow { + val selectableSubInfoList = + onboardingService.getSelectedSubscriptionInfoListWithRenaming() + emit(PrimarySimRepository(context).getPrimarySimInfo(selectableSubInfoList)) + }.flowOn(Dispatchers.Default) + }.collectAsStateWithLifecycle(initialValue = null).value ?: return@SuwScaffold callsSelectedId.intValue = onboardingService.targetPrimarySimCalls textsSelectedId.intValue = onboardingService.targetPrimarySimTexts mobileDataSelectedId.intValue = onboardingService.targetPrimarySimMobileData PrimarySimImpl( - subscriptionInfoList = selectedSubscriptionInfoList, + primarySimInfo = primarySimInfo, callsSelectedId = callsSelectedId, textsSelectedId = textsSelectedId, mobileDataSelectedId = mobileDataSelectedId, diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 334ca61bc9b..9e4cf9f4c12 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -36,10 +36,10 @@ import com.android.settings.network.telephony.isSubscriptionEnabledFlow import com.android.settings.network.telephony.phoneNumberFlow import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel -import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference import com.android.settingslib.spa.widget.ui.SettingsIcon import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference +import com.android.settingslib.spaprivileged.template.preference.RestrictedTwoTargetSwitchPreference @Composable fun SimsSection(subscriptionInfoList: List) { @@ -61,9 +61,8 @@ private fun SimPreference(subInfo: SubscriptionInfo) { val phoneNumber = remember(subInfo) { context.phoneNumberFlow(subInfo) }.collectAsStateWithLifecycle(initialValue = null) - //TODO: Add the Restricted TwoTargetSwitchPreference in SPA - TwoTargetSwitchPreference( - object : SwitchPreferenceModel { + RestrictedTwoTargetSwitchPreference( + model = object : SwitchPreferenceModel { override val title = subInfo.displayName.toString() override val summary = { phoneNumber.value ?: "" } override val checked = { checked.value } @@ -74,7 +73,8 @@ private fun SimPreference(subInfo: SubscriptionInfo) { newChecked, ) } - } + }, + restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)), ) { MobileNetworkUtils.launchMobileNetworkSettings(context, subInfo) } diff --git a/tests/robotests/src/com/android/settings/applications/AppInfoWithHeaderTest.java b/tests/robotests/src/com/android/settings/applications/AppInfoWithHeaderTest.java index ce520271de6..562212e3569 100644 --- a/tests/robotests/src/com/android/settings/applications/AppInfoWithHeaderTest.java +++ b/tests/robotests/src/com/android/settings/applications/AppInfoWithHeaderTest.java @@ -19,6 +19,7 @@ package com.android.settings.applications; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,12 +49,13 @@ import com.android.settingslib.widget.LayoutPreference; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -62,6 +64,8 @@ import org.robolectric.util.ReflectionHelpers; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowEntityHeaderController.class, ShadowSettingsLibUtils.class}) public class AppInfoWithHeaderTest { + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock(answer = Answers.RETURNS_DEEP_STUBS) private EntityHeaderController mHeaderController; @@ -71,7 +75,6 @@ public class AppInfoWithHeaderTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); mFactory = FakeFeatureFactory.setupForTest(); when(mFactory.metricsFeatureProvider.getMetricsCategory(any(Object.class))) .thenReturn(MetricsProto.MetricsEvent.SETTINGS_APP_NOTIF_CATEGORY); @@ -120,7 +123,6 @@ public class AppInfoWithHeaderTest { assertThat(mAppInfoWithHeader.mPackageRemovedCalled).isTrue(); } - @Ignore("b/315135755") @Test public void noExtraUserHandleInIntent_retrieveAppEntryWithMyUserId() throws PackageManager.NameNotFoundException { @@ -133,10 +135,8 @@ public class AppInfoWithHeaderTest { when(mAppInfoWithHeader.mState.getEntry(packageName, UserHandle.myUserId())).thenReturn(entry); - when(mAppInfoWithHeader.mPm.getPackageInfoAsUser(entry.info.packageName, - PackageManager.MATCH_DISABLED_COMPONENTS | - PackageManager.GET_SIGNING_CERTIFICATES | - PackageManager.GET_PERMISSIONS, UserHandle.myUserId())).thenReturn( + when(mAppInfoWithHeader.mPm.getPackageInfoAsUser(eq(entry.info.packageName), + any(), eq(UserHandle.myUserId()))).thenReturn( mAppInfoWithHeader.mPackageInfo); mAppInfoWithHeader.retrieveAppEntry(); @@ -146,7 +146,6 @@ public class AppInfoWithHeaderTest { assertThat(mAppInfoWithHeader.mAppEntry).isNotNull(); } - @Ignore("b/315135755") @Test public void extraUserHandleInIntent_retrieveAppEntryWithMyUserId() throws PackageManager.NameNotFoundException { @@ -161,10 +160,8 @@ public class AppInfoWithHeaderTest { entry.info.packageName = packageName; when(mAppInfoWithHeader.mState.getEntry(packageName, USER_ID)).thenReturn(entry); - when(mAppInfoWithHeader.mPm.getPackageInfoAsUser(entry.info.packageName, - PackageManager.MATCH_DISABLED_COMPONENTS | - PackageManager.GET_SIGNING_CERTIFICATES | - PackageManager.GET_PERMISSIONS, USER_ID)).thenReturn( + when(mAppInfoWithHeader.mPm.getPackageInfoAsUser(eq(entry.info.packageName), + any(), eq(USER_ID))).thenReturn( mAppInfoWithHeader.mPackageInfo); mAppInfoWithHeader.retrieveAppEntry(); @@ -232,6 +229,8 @@ public class AppInfoWithHeaderTest { } @Override - protected Intent getIntent() { return mIntent; } + protected Intent getIntent() { + return mIntent; + } } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java new file mode 100644 index 00000000000..c08bb98e55b --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import static android.bluetooth.BluetoothCsipSetCoordinator.GROUP_ID_INVALID; +import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; + +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingAidsPresetsController.KEY_HEARING_AIDS_PRESETS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; +import android.bluetooth.BluetoothHapPresetInfo; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceCategory; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +/** Tests for {@link BluetoothDetailsHearingAidsPresetsController}. */ +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingAidsPresetsControllerTest extends + BluetoothDetailsControllerTestBase { + + private static final int TEST_PRESET_INDEX = 1; + private static final String TEST_PRESET_NAME = "test_preset"; + private static final int TEST_HAP_GROUP_ID = 1; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private HapClientProfile mHapClientProfile; + @Mock + private CachedBluetoothDevice mCachedChildDevice; + @Mock + private BluetoothDevice mChildDevice; + + private BluetoothDetailsHearingAidsPresetsController mController; + + @Override + public void setUp() { + super.setUp(); + + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); + when(mCachedChildDevice.getDevice()).thenReturn(mChildDevice); + PreferenceCategory deviceControls = new PreferenceCategory(mContext); + deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); + mScreen.addPreference(deviceControls); + mController = new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, + mLocalManager, mCachedDevice, mLifecycle); + mController.init(mScreen); + } + + @Test + public void isAvailable_supportHap_returnTrue() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_notSupportHap_returnFalse() { + when(mCachedDevice.getProfiles()).thenReturn(new ArrayList<>()); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void onResume_registerCallback() { + mController.onResume(); + + verify(mHapClientProfile).registerCallback(any(Executor.class), + any(BluetoothHapClient.Callback.class)); + } + + @Test + public void onPause_unregisterCallback() { + mController.onPause(); + + verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class)); + } + + @Test + public void onPreferenceChange_keyMatched_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + + boolean handled = mController.onPreferenceChange(presetPreference, + String.valueOf(TEST_PRESET_INDEX)); + + assertThat(handled).isTrue(); + verify(presetPreference).setSummary(TEST_PRESET_NAME); + } + + @Test + public void onPreferenceChange_keyNotMatched_doNothing() { + final ListPreference presetPreference = getTestPresetPreference("wrong_key"); + + boolean handled = mController.onPreferenceChange( + presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + assertThat(handled).isFalse(); + verify(presetPreference, never()).setSummary(any()); + } + + @Test + public void onPreferenceChange_supportGroupOperation_validGroupId_verifySelectPresetForGroup() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_verifySelectPreset() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_invalidGroupId_verifySelectPreset() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(GROUP_ID_INVALID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_hasSubDevice_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mCachedDevice.getSubDevice()).thenReturn(mCachedChildDevice); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mChildDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_hasMemberDevice_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedChildDevice)); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mChildDevice, TEST_PRESET_INDEX); + } + + @Test + public void refresh_emptyPresetInfo_preferenceDisabled() { + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(new ArrayList<>()); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().isEnabled()).isFalse(); + } + + @Test + public void refresh_validPresetInfo_preferenceEnabled() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().isEnabled()).isTrue(); + } + + @Test + public void refresh_invalidActivePresetIndex_summaryIsNull() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(PRESET_INDEX_UNAVAILABLE); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().getSummary()).isNull(); + } + + @Test + public void refresh_validActivePresetIndex_summaryIsNotNull() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(TEST_PRESET_INDEX); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().getSummary()).isNotNull(); + } + + private BluetoothHapPresetInfo getTestPresetInfo() { + BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class); + when(info.getName()).thenReturn(TEST_PRESET_NAME); + when(info.getIndex()).thenReturn(TEST_PRESET_INDEX); + return info; + } + + private ListPreference getTestPresetPreference(String key) { + final ListPreference presetPreference = spy(new ListPreference(mContext)); + when(presetPreference.findIndexOfValue(String.valueOf(TEST_PRESET_INDEX))).thenReturn(0); + when(presetPreference.getEntries()).thenReturn(new CharSequence[]{TEST_PRESET_NAME}); + when(presetPreference.getEntryValues()).thenReturn( + new CharSequence[]{String.valueOf(TEST_PRESET_INDEX)}); + presetPreference.setKey(key); + return presetPreference; + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java new file mode 100644 index 00000000000..2a50f892add --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import com.android.settings.accessibility.Flags; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link BluetoothDetailsHearingDeviceController}. */ +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingDeviceControllerTest extends + BluetoothDetailsControllerTestBase { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private BluetoothDetailsHearingDeviceController mHearingDeviceController; + @Mock + private BluetoothDetailsHearingAidsPresetsController mPresetsController; + @Mock + private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; + + @Override + public void setUp() { + super.setUp(); + + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, + mFragment, mLocalManager, mCachedDevice, mLifecycle); + mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController, + mPresetsController); + } + + @Test + public void isAvailable_hearingDeviceSettingsAvailable_returnTrue() { + when(mHearingDeviceSettingsController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_presetsControlsAvailable_returnTrue() { + when(mPresetsController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_noControllersAvailable_returnFalse() { + when(mHearingDeviceSettingsController.isAvailable()).thenReturn(false); + when(mPresetsController.isAvailable()).thenReturn(false); + + assertThat(mHearingDeviceController.isAvailable()).isFalse(); + } + + + @Test + public void initSubControllers_launchFromHearingDevicePage_hearingDeviceSettingsNotExist() { + mHearingDeviceController.initSubControllers(true); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceSettingsController)).isFalse(); + } + + @Test + public void initSubControllers_notLaunchFromHearingDevicePage_hearingDeviceSettingsExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceSettingsController)).isTrue(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HEARING_AID_PRESET_CONTROL) + public void initSubControllers_flagEnabled_presetControllerExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingAidsPresetsController)).isTrue(); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_HEARING_AID_PRESET_CONTROL) + public void initSubControllers_flagDisabled_presetControllerNotExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingAidsPresetsController)).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java similarity index 81% rename from tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java rename to tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java index 364d299e519..b420717d397 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java @@ -39,23 +39,24 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -/** Tests for {@link BluetoothDetailsHearingDeviceControlsController}. */ +/** Tests for {@link BluetoothDetailsHearingDeviceSettingsController}. */ @RunWith(RobolectricTestRunner.class) -public class BluetoothDetailsHearingDeviceControlsControllerTest extends +public class BluetoothDetailsHearingDeviceSettingsControllerTest extends BluetoothDetailsControllerTestBase { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Captor private ArgumentCaptor mIntentArgumentCaptor; - private BluetoothDetailsHearingDeviceControlsController mController; + private BluetoothDetailsHearingDeviceSettingsController mController; @Override public void setUp() { super.setUp(); FakeFeatureFactory.setupForTest(); - mController = new BluetoothDetailsHearingDeviceControlsController(mActivity, mFragment, + mController = new BluetoothDetailsHearingDeviceSettingsController(mActivity, mFragment, mCachedDevice, mLifecycle); when(mCachedDevice.isHearingAidDevice()).thenReturn(true); } @@ -75,12 +76,12 @@ public class BluetoothDetailsHearingDeviceControlsControllerTest extends } @Test - public void onPreferenceClick_hearingDeviceControlsKey_LaunchExpectedFragment() { - final Preference hearingControlsKeyPreference = new Preference(mContext); - hearingControlsKeyPreference.setKey( - BluetoothDetailsHearingDeviceControlsController.KEY_HEARING_DEVICE_CONTROLS); + public void onPreferenceClick_hearingDeviceSettingsKey_launchExpectedFragment() { + final Preference hearingDeviceSettingsPreference = new Preference(mContext); + hearingDeviceSettingsPreference.setKey( + BluetoothDetailsHearingDeviceSettingsController.KEY_HEARING_DEVICE_SETTINGS); - mController.onPreferenceClick(hearingControlsKeyPreference); + mController.onPreferenceClick(hearingDeviceSettingsPreference); assertStartActivityWithExpectedFragment(mActivity, AccessibilityHearingAidsFragment.class.getName()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java index fc72c412b6e..50aa7719ccb 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java @@ -18,7 +18,7 @@ package com.android.settings.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_NONE; -import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceControlsController.KEY_DEVICE_CONTROLS_GENERAL_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceSettingsController.KEY_HEARING_DEVICE_SETTINGS; import static com.google.common.truth.Truth.assertThat; @@ -237,7 +237,7 @@ public class BluetoothDeviceDetailsFragmentTest { assertThat(controllerList.stream() .anyMatch(controller -> controller.getPreferenceKey().equals( - KEY_DEVICE_CONTROLS_GENERAL_GROUP))).isFalse(); + KEY_HEARING_DEVICE_SETTINGS))).isFalse(); } @Test @@ -253,7 +253,7 @@ public class BluetoothDeviceDetailsFragmentTest { assertThat(controllerList.stream() .anyMatch(controller -> controller.getPreferenceKey().equals( - KEY_DEVICE_CONTROLS_GENERAL_GROUP))).isTrue(); + KEY_HEARING_DEVICE_SETTINGS))).isTrue(); } private InputDevice createInputDeviceWithMatchingBluetoothAddress() { diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java index 0648de4ad69..80739e9d47a 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java @@ -60,12 +60,8 @@ import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.instantapps.InstantAppDataProvider; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.datastore.ChangeReason; -import com.android.settingslib.datastore.Observer; import com.android.settingslib.widget.LayoutPreference; -import com.google.common.util.concurrent.MoreExecutors; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -119,10 +115,8 @@ public class AdvancedPowerUsageDetailTest { @Mock private AppOpsManager mAppOpsManager; @Mock private LoaderManager mLoaderManager; @Mock private BatteryOptimizeUtils mBatteryOptimizeUtils; - @Mock private Observer mObserver; private Context mContext; - private BatterySettingsStorage mBatterySettingsStorage; private PrimarySwitchPreference mAllowBackgroundUsagePreference; private AdvancedPowerUsageDetail mFragment; private SettingsActivity mTestActivity; @@ -134,7 +128,6 @@ public class AdvancedPowerUsageDetailTest { @Before public void setUp() { mContext = spy(ApplicationProvider.getApplicationContext()); - mBatterySettingsStorage = BatterySettingsStorage.get(mContext); when(mContext.getPackageName()).thenReturn("foo"); mFeatureFactory = FakeFeatureFactory.setupForTest(); mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider; @@ -448,28 +441,4 @@ public class AdvancedPowerUsageDetailTest { TimeUnit.SECONDS.sleep(1); verifyNoInteractions(mMetricsFeatureProvider); } - - @Test - public void notifyBackupManager_optimizationModeIsNotChanged_notInvokeDataChanged() { - mBatterySettingsStorage.addObserver(mObserver, MoreExecutors.directExecutor()); - final int mode = BatteryOptimizeUtils.MODE_RESTRICTED; - mFragment.mOptimizationMode = mode; - when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode); - - mFragment.notifyBackupManager(); - - verifyNoInteractions(mObserver); - } - - @Test - public void notifyBackupManager_optimizationModeIsChanged_invokeDataChanged() { - mBatterySettingsStorage.addObserver(mObserver, MoreExecutors.directExecutor()); - mFragment.mOptimizationMode = BatteryOptimizeUtils.MODE_RESTRICTED; - when(mBatteryOptimizeUtils.getAppOptimizationMode()) - .thenReturn(BatteryOptimizeUtils.MODE_UNRESTRICTED); - - mFragment.notifyBackupManager(); - - verify(mObserver).onChanged(ChangeReason.UPDATE); - } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java index 3551eeb431e..6085b9a3ce4 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java @@ -49,8 +49,12 @@ import android.os.UserManager; import android.util.ArraySet; import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action; +import com.android.settingslib.datastore.ChangeReason; +import com.android.settingslib.datastore.Observer; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; +import com.google.common.util.concurrent.MoreExecutors; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,14 +78,18 @@ public class BatteryOptimizeUtilsTest { @Mock private PowerAllowlistBackend mMockBackend; @Mock private IPackageManager mMockIPackageManager; @Mock private UserManager mMockUserManager; + @Mock private Observer mObserver; private Context mContext; private BatteryOptimizeUtils mBatteryOptimizeUtils; + private BatterySettingsStorage mBatterySettingsStorage; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); + mBatterySettingsStorage = BatterySettingsStorage.get(mContext); + mBatterySettingsStorage.addObserver(mObserver, MoreExecutors.directExecutor()); mBatteryOptimizeUtils = spy(new BatteryOptimizeUtils(mContext, UID, PACKAGE_NAME)); mBatteryOptimizeUtils.mAppOpsManager = mMockAppOpsManager; mBatteryOptimizeUtils.mBatteryUtils = mMockBatteryUtils; @@ -156,6 +164,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_IGNORED, /* allowListed */ false); + verify(mObserver).onChanged(ChangeReason.UPDATE); } @Test @@ -169,6 +178,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ true); + verify(mObserver).onChanged(ChangeReason.UPDATE); } @Test @@ -182,6 +192,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); + verify(mObserver).onChanged(ChangeReason.UPDATE); } @Test @@ -197,6 +208,7 @@ public class BatteryOptimizeUtilsTest { verify(mMockBatteryUtils, never()).setForceAppStandby(anyInt(), anyString(), anyInt()); verify(mMockBackend, never()).addApp(anyString()); verify(mMockBackend, never()).removeApp(anyString()); + verifyNoInteractions(mObserver); } @Test @@ -288,6 +300,7 @@ public class BatteryOptimizeUtilsTest { inOrder.verify(mMockBackend).isAllowlisted(PACKAGE_NAME, UID); inOrder.verify(mMockBackend).isSysAllowlisted(PACKAGE_NAME); verifyNoMoreInteractions(mMockBackend); + verify(mObserver).onChanged(ChangeReason.DELETE); } @Test @@ -298,6 +311,7 @@ public class BatteryOptimizeUtilsTest { /* isSystemOrDefaultApp */ false); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); + verify(mObserver).onChanged(ChangeReason.DELETE); } @Test @@ -308,6 +322,7 @@ public class BatteryOptimizeUtilsTest { /* isSystemOrDefaultApp */ false); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); + verify(mObserver).onChanged(ChangeReason.DELETE); } private void runTestForResetWithMode(