diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fd40e905fda..cc4d898403a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5362,6 +5362,14 @@ + + + + + + diff --git a/res/drawable/ic_android_satellite_24px.xml b/res/drawable/ic_android_satellite_24px.xml index 15f28840069..b08f19bee02 100644 --- a/res/drawable/ic_android_satellite_24px.xml +++ b/res/drawable/ic_android_satellite_24px.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:tint="?android:attr/colorControlNormal"> diff --git a/res/values/strings.xml b/res/values/strings.xml index cd847f7400e..20b70aae915 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8030,10 +8030,10 @@ Off - Off + Fixed - Off + Head Tracking @@ -8249,8 +8249,7 @@ enable dark theme - Switch the OS and apps to prefer light text on a dark - background, which may be easier on the eyes and confers significant battery savings on some devices + Switch the device theme to use a dark background, which is easier on the eyes {count, plural, offset:2 diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml index e4ebe788b0c..292f1824552 100644 --- a/res/xml/network_provider_internet.xml +++ b/res/xml/network_provider_internet.xml @@ -52,9 +52,8 @@ android:order="-15" settings:keywords="@string/keywords_more_mobile_networks" settings:userRestriction="no_config_mobile_networks" - settings:isPreferenceVisible="@bool/config_show_sim_info" settings:useAdminDisabledSummary="true" - settings:searchable="@bool/config_show_sim_info"/> + settings:controller="com.android.settings.network.MobileNetworkSummaryController" /> mDevicesWithMetadataChangedListener = + new CopyOnWriteArrayList<>(); + + // BluetoothDevicePreference updates the summary based on several callbacks, including + // BluetoothAdapter.OnMetadataChangedListener and BluetoothCallback. In most cases, + // metadata changes callback will be triggered before onDeviceBondStateChanged(BOND_BONDED). + // And before we hear onDeviceBondStateChanged(BOND_BONDED), the BluetoothDevice.getState() has + // already been BOND_BONDED. These event sequence will lead to: before we hear + // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already + // change from "Pairing..." to empty since it listens to metadata changes happens earlier. + // + // In share then pair flow, we have to wait on this page till the device is connected. + // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and + // "Connecting..." To help users better understand the process, we listen to metadata change + // as well and show a loading dialog with "Connecting to ...." once BluetoothDevice.getState() + // gets to BOND_BONDED. + final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = + new BluetoothAdapter.OnMetadataChangedListener() { + @Override + public void onMetadataChanged(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); + if (mShouldTriggerAudioSharingShareThenPairFlow && mLoadingDialog == null + && device.getBondState() == BluetoothDevice.BOND_BONDED + && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + // Once device is bonded, remove the listener + removeOnMetadataChangedListener(device); + } + } + }; public BluetoothDevicePairingDetailBase() { super(DISALLOW_CONFIG_BLUETOOTH); @@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); + mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); } @Override @@ -80,6 +143,26 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere disableScanning(); } + @Override + public void onDestroy() { + super.onDestroy(); + var unused = ThreadUtils.postOnBackgroundThread(() -> { + mDevicesWithMetadataChangedListener.forEach( + device -> { + try { + if (mBluetoothAdapter != null) { + mBluetoothAdapter.removeOnMetadataChangedListener(device, + mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + }); + mDevicesWithMetadataChangedListener.clear(); + }); + } + @Override public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); @@ -92,16 +175,37 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { if (bondState == BluetoothDevice.BOND_BONDED) { + if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + removeOnMetadataChangedListener(device); + return; + } + } // If one device is connected(bonded), then close this fragment. finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + addOnMetadataChangedListener(device); + } + } // Set the bond entry where binding process starts for logging hearing aid device info final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() .getAttribution(getActivity()); final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry( pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); + } else if (bondState == BluetoothDevice.BOND_NONE) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + removeOnMetadataChangedListener(device); + } + } } if (mSelectedDevice != null && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); @@ -114,7 +218,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + public void onProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile) { // This callback is used to handle the case that bonded device is connected in pairing list. // 1. If user selected multiple bonded devices in pairing list, after connected @@ -123,8 +228,22 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // removed from paring list. if (cachedDevice != null && cachedDevice.isConnected()) { final BluetoothDevice device = cachedDevice.getDevice(); - if (device != null && mSelectedList.contains(device)) { - finish(); + if (device != null + && mSelectedList.contains(device)) { + if (!BluetoothUtils.isAudioSharingEnabled()) { + finish(); + return; + } + if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && state == BluetoothAdapter.STATE_CONNECTED + && device.equals(mJustBonded) + && mShouldTriggerAudioSharingShareThenPairFlow) { + Log.d(getLogTag(), + "onProfileConnectionStateChanged, assistant profile connected"); + dismissConnectingDialog(); + mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); + finishFragmentWithResultForAudioSharing(device); + } } else { onDeviceDeleted(cachedDevice); } @@ -148,6 +267,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { disableScanning(); super.onDevicePreferenceClick(btPreference); + // Clean up the previous bond value + mJustBonded = null; } @VisibleForTesting @@ -165,8 +286,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere * {@code bluetoothState} is off. * * @param bluetoothState the current Bluetooth state, the possible values that will handle here: - * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, - * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, + * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, + * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, */ @VisibleForTesting public void updateContent(int bluetoothState) { @@ -187,4 +308,122 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, Toast.LENGTH_SHORT).show(); } + + @VisibleForTesting + boolean shouldTriggerAudioSharingShareThenPairFlow() { + if (!BluetoothUtils.isAudioSharingEnabled()) return false; + Activity activity = getActivity(); + Intent intent = activity == null ? null : activity.getIntent(); + Bundle args = + intent == null ? null : + intent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + return args != null + && args.getBoolean(EXTRA_PAIR_AND_JOIN_SHARING, false); + } + + private void addOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && !mDevicesWithMetadataChangedListener.contains(device)) { + mBluetoothAdapter.addOnMetadataChangedListener(device, mExecutor, + mMetadataListener); + mDevicesWithMetadataChangedListener.add(device); + } + }); + } + + private void removeOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && mDevicesWithMetadataChangedListener.contains(device)) { + try { + mBluetoothAdapter.removeOnMetadataChangedListener(device, mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + } + }); + } + + private void triggerAudioSharingShareThenPairFlow( + @NonNull BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mJustBonded != null) { + Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); + return; + } + mJustBonded = device; + // Show connecting device loading state + String aliasName = device.getAlias(); + String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() + : aliasName; + showConnectingDialog("Connecting to " + deviceName + "..."); + // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio + // sharing. + if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { + mHandler.postDelayed(() -> + postOnMainThread( + () -> { + Log.d(getLogTag(), "Show incompatible dialog when timeout"); + dismissConnectingDialog(); + AudioSharingIncompatibleDialogFragment.show(this, deviceName, + () -> finish()); + }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + }); + } + + private void finishFragmentWithResultForAudioSharing(@Nullable BluetoothDevice device) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + if (getActivity() != null) { + getActivity().setResult(Activity.RESULT_OK, resultIntent); + } + finish(); + } + + // TODO: use DialogFragment + private void showConnectingDialog(@NonNull String message) { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + Log.d(getLogTag(), "showConnectingDialog, is already showing"); + TextView textView = mLoadingDialog.findViewById(R.id.message); + if (textView != null && !message.equals(textView.getText().toString())) { + Log.d(getLogTag(), "showConnectingDialog, update message"); + // TODO: use string res once finalized + textView.setText(message); + } + return; + } + Log.d(getLogTag(), "showConnectingDialog, show dialog"); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + View customView = inflater.inflate( + R.layout.dialog_audio_sharing_loading_state, /* root= */ + null); + TextView textView = customView.findViewById(R.id.message); + if (textView != null) { + // TODO: use string res once finalized + textView.setText(message); + } + AlertDialog dialog = builder.setView(customView).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + mLoadingDialog = dialog; + dialog.show(); + }); + } + + private void dismissConnectingDialog() { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + mLoadingDialog.dismiss(); + } + }); + } + + private void postOnMainThread(@NonNull Runnable runnable) { + getContext().getMainExecutor().execute(runnable); + } } diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt index d29795efee7..9743737f515 100644 --- a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -62,6 +63,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties @@ -78,7 +80,11 @@ fun MultiTogglePreferenceGroup( var settingIdForPopUp by remember { mutableStateOf(null) } settingIdForPopUp?.let { id -> - preferenceModels.find { it.id == id }?.let { dialog(it) { settingIdForPopUp = null } } + preferenceModels.find { it.id == id && it.isAllowedChangingState }?.let { + dialog(it) { settingIdForPopUp = null } + } ?: run { + settingIdForPopUp = null + } } Row( @@ -102,7 +108,9 @@ fun MultiTogglePreferenceGroup( Modifier.fillMaxSize().padding(8.dp).semantics { role = Role.Switch toggleableState = - if (preferenceModel.isActive) { + if (!preferenceModel.isAllowedChangingState) { + ToggleableState.Indeterminate + } else if (preferenceModel.isActive) { ToggleableState.On } else { ToggleableState.Off @@ -110,6 +118,7 @@ fun MultiTogglePreferenceGroup( contentDescription = preferenceModel.title }, onClick = { settingIdForPopUp = preferenceModel.id }, + enabled = preferenceModel.isAllowedChangingState, shape = RoundedCornerShape(20.dp), colors = getButtonColors(preferenceModel.isActive), contentPadding = PaddingValues(0.dp)) { @@ -254,7 +263,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.Mu } Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth().height(32.dp), + modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { @@ -263,6 +272,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.Mu text = toggle.label, fontSize = 12.sp, textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java index ad41e8a2997..786e1dccc05 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java @@ -16,10 +16,18 @@ package com.android.settings.connecteddevice.audiosharing; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.os.Bundle; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; @@ -27,16 +35,21 @@ import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.utils.ThreadUtils; public class AudioSharingDashboardFragment extends DashboardFragment implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener { private static final String TAG = "AudioSharingDashboardFrag"; + public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002; + SettingsMainSwitchBar mMainSwitchBar; private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController; private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController; private AudioStreamsCategoryController mAudioStreamsCategoryController; + private AudioSharingSwitchBarController mAudioSharingSwitchBarController; public AudioSharingDashboardFragment() { super(); @@ -84,13 +97,38 @@ public class AudioSharingDashboardFragment extends DashboardFragment final SettingsActivity activity = (SettingsActivity) getActivity(); mMainSwitchBar = activity.getSwitchBar(); mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title)); - AudioSharingSwitchBarController switchBarController = + mAudioSharingSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this); - switchBarController.init(this); - getSettingsLifecycle().addObserver(switchBarController); + mAudioSharingSwitchBarController.init(this); + getSettingsLifecycle().addObserver(mAudioSharingSwitchBarController); mMainSwitchBar.show(); } + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (!BluetoothUtils.isAudioSharingEnabled()) return; + // In share then pair flow, after users be routed to pair new device page and successfully + // pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK + // and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller, + // which is responsible for adding source to the device with loading indicator. + if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + BluetoothDevice btDevice = + data != null + ? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice); + if (btDevice != null) { + var unused = ThreadUtils.postOnBackgroundThread( + () -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair( + btDevice)); + } + } + } + } + @Override public void onAudioSharingStateChanged() { updateVisibilityForAttachedPreferences(); @@ -107,11 +145,13 @@ public class AudioSharingDashboardFragment extends DashboardFragment AudioSharingDeviceVolumeGroupController volumeGroupController, AudioSharingCallAudioPreferenceController callAudioController, AudioSharingPlaySoundPreferenceController playSoundController, - AudioStreamsCategoryController streamsCategoryController) { + AudioStreamsCategoryController streamsCategoryController, + AudioSharingSwitchBarController switchBarController) { mAudioSharingDeviceVolumeGroupController = volumeGroupController; mAudioSharingCallAudioPreferenceController = callAudioController; mAudioSharingPlaySoundPreferenceController = playSoundController; mAudioStreamsCategoryController = streamsCategoryController; + mAudioSharingSwitchBarController = switchBarController; } private void updateVisibilityForAttachedPreferences() { diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 1ae541ca01d..1b68eaccbfe 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -16,6 +16,9 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import android.app.Dialog; import android.app.settings.SettingsEnums; import android.os.Bundle; @@ -48,19 +51,23 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { // The host creates an instance of this dialog fragment must implement this interface to receive // event callbacks. public interface DialogEventListener { + /** Called when users click the positive button in the dialog. */ + default void onPositiveClick() {} + /** * Called when users click the device item for sharing in the dialog. * * @param item The device item clicked. */ - void onItemClick(AudioSharingDeviceItem item); + default void onItemClick(@NonNull AudioSharingDeviceItem item) {} /** Called when users click the cancel button in the dialog. */ - void onCancelClick(); + default void onCancelClick() {} } @Nullable private static DialogEventListener sListener; private static Pair[] sEventData = new Pair[0]; + @Nullable private static Fragment sHost; @Override public int getMetricsCategory() { @@ -70,10 +77,10 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { /** * Display the {@link AudioSharingDialogFragment} dialog. * - * @param host The Fragment this dialog will be hosted. + * @param host The Fragment this dialog will be hosted. * @param deviceItems The connected device items eligible for audio sharing. - * @param listener The callback to handle the user action on this dialog. - * @param eventData The eventData to log with for dialog onClick events. + * @param listener The callback to handle the user action on this dialog. + * @param eventData The eventData to log with for dialog onClick events. */ public static void show( @NonNull Fragment host, @@ -88,6 +95,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + sHost = host; sListener = listener; sEventData = eventData; AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); @@ -136,23 +144,33 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { .setCustomPositiveButton( R.string.audio_sharing_pair_button_label, v -> { - dismiss(); - new SubSettingLauncher(getContext()) - .setDestination(BluetoothPairingDetail.class.getName()) - .setSourceMetricsCategory(getMetricsCategory()) - .launch(); + if (sListener != null) { + sListener.onPositiveClick(); + } logDialogPositiveBtnClick(); + dismiss(); + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, true); + SubSettingLauncher launcher = + new SubSettingLauncher(getContext()) + .setDestination( + BluetoothPairingDetail.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(args); + if (sHost != null) { + launcher.setResultListener(sHost, SHARE_THEN_PAIR_REQUEST_CODE); + } + launcher.launch(); }) .setCustomNegativeButton( R.string.audio_sharing_qrcode_button_label, v -> { - dismiss(); + onCancelClick(); new SubSettingLauncher(getContext()) .setTitleRes(R.string.audio_streams_qr_code_page_title) .setDestination(AudioStreamsQrCodeFragment.class.getName()) .setSourceMetricsCategory(getMetricsCategory()) .launch(); - logDialogNegativeBtnClick(); }); } else if (deviceItems.size() == 1) { AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems); @@ -166,8 +184,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { v -> { if (sListener != null) { sListener.onItemClick(deviceItem); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }) .setCustomNegativeButton( @@ -182,8 +200,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { (AudioSharingDeviceItem item) -> { if (sListener != null) { sListener.onItemClick(item); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }, AudioSharingDeviceAdapter.ActionType.SHARE)) @@ -196,8 +214,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private void onCancelClick() { if (sListener != null) { sListener.onCancelClick(); - logDialogNegativeBtnClick(); } + logDialogNegativeBtnClick(); dismiss(); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java index 5de615e968f..aceeb94420e 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingIncompatDlg"; @@ -59,7 +58,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr * * @param host The Fragment this dialog will be hosted. */ - public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, + public static void show(@Nullable Fragment host, @NonNull String deviceName, @NonNull DialogEventListener listener) { if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; final FragmentManager manager; @@ -77,7 +76,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr } Log.d(TAG, "Show up the incompatible device dialog."); final Bundle bundle = new Bundle(); - bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); + bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName); AudioSharingIncompatibleDialogFragment dialogFrag = new AudioSharingIncompatibleDialogFragment(); dialogFrag.setArguments(bundle); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java index 79cc56ea6dd..8706590c362 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java @@ -115,10 +115,6 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr @NonNull public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mHandler = new Handler(Looper.getMainLooper()); - mHandler.postDelayed(() -> { - Log.d(TAG, "Auto dismiss dialog after timeout"); - dismiss(); - }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); Bundle args = requireArguments(); String message = args.getString(BUNDLE_KEY_MESSAGE, ""); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); @@ -132,6 +128,26 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr return dialog; } + @Override + public void onStart() { + super.onStart(); + if (mHandler != null) { + Log.d(TAG, "onStart, postTimeOut for auto dismiss"); + mHandler.postDelayed(() -> { + Log.d(TAG, "Try to auto dismiss dialog after timeout"); + try { + Dialog dialog = getDialog(); + if (dialog != null) { + Log.d(TAG, "Dialog is not null, dismiss"); + dismissAllowingStateLoss(); + } + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to dismiss: " + e.getMessage()); + } + }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + } + @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index c0f463d863f..395647ca84a 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -56,6 +56,7 @@ import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -78,9 +79,9 @@ import java.util.concurrent.atomic.AtomicInteger; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, - OnCheckedChangeListener, - LocalBluetoothProfileManager.ServiceListener, - BluetoothCallback { + OnCheckedChangeListener, + LocalBluetoothProfileManager.ServiceListener, + BluetoothCallback { private static final String TAG = "AudioSharingSwitchCtlr"; private static final String PREF_KEY = "audio_sharing_main_switch"; @@ -464,6 +465,18 @@ public class AudioSharingSwitchBarController extends BasePreferenceController this.mFragment = fragment; } + /** Handle auto add source to the just paired device in share then pair flow. */ + public void handleAutoAddSourceAfterPair(@NonNull BluetoothDevice device) { + CachedBluetoothDeviceManager deviceManager = + mBtManager == null ? null : mBtManager.getCachedDeviceManager(); + CachedBluetoothDevice cachedDevice = + deviceManager == null ? null : deviceManager.findDevice(device); + if (cachedDevice != null) { + Log.d(TAG, "handleAutoAddSourceAfterPair, device = " + device.getAnonymizedAddress()); + addSourceToTargetSinks(ImmutableList.of(device), cachedDevice.getName()); + } + } + /** Test only: set callback registration status in tests. */ @VisibleForTesting void setCallbacksRegistered(boolean registered) { @@ -610,8 +623,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING); mTargetActiveItem = null; if (mIntentHandleStage.compareAndSet( - StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), - StartIntentHandleStage.HANDLED.ordinal()) + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()) && mDeviceItemsForSharing.size() == 1) { Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device"); AudioSharingDeviceItem target = mDeviceItemsForSharing.get(0); @@ -638,6 +651,13 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private void showDialog(Pair[] eventData) { AudioSharingDialogFragment.DialogEventListener listener = new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + // Could go to other pages, dismiss the loading dialog. + dismissLoadingStateDialogIfNeeded(); + cleanUp(); + } + @Override public void onItemClick(@NonNull AudioSharingDeviceItem item) { List targetSinks = mGroupedConnectedDevices.getOrDefault( @@ -648,6 +668,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @Override public void onCancelClick() { + // Could go to other pages, dismiss the loading dialog. dismissLoadingStateDialogIfNeeded(); cleanUp(); } @@ -669,8 +690,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @NonNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && (event.getContentChangeTypes() - & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) - != 0) { + & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) + != 0) { Log.d(TAG, "Skip accessibility event for CONTENT_CHANGE_TYPE_ENABLED"); return false; } diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index ef129ff3d24..907fe7bd722 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -16,6 +16,8 @@ package com.android.settings.localepicker; +import static com.google.common.base.Preconditions.checkNotNull; + import android.app.settings.SettingsEnums; import android.content.Context; import android.graphics.Canvas; @@ -41,7 +43,8 @@ import com.android.internal.app.LocalePicker; import com.android.internal.app.LocaleStore; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.shortcut.ShortcutsUpdateTask; +import com.android.settings.shortcut.ShortcutsUpdater; +import com.android.settingslib.utils.ThreadUtils; import java.text.NumberFormat; import java.util.ArrayList; @@ -96,7 +99,7 @@ class LocaleDragAndDropAdapter LocaleDragAndDropAdapter(LocaleListEditor parent, List feedItemList) { mFeedItemList = feedItemList; mCacheItemList = new ArrayList<>(feedItemList); - mContext = parent.getContext(); + mContext = checkNotNull(parent.getContext()); final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, mContext.getResources().getDisplayMetrics()); @@ -350,7 +353,8 @@ class LocaleDragAndDropAdapter LocalePicker.updateLocales(mLocalesToSetNext); mLocalesSetLast = mLocalesToSetNext; - new ShortcutsUpdateTask(mContext).execute(); + ThreadUtils.postOnBackgroundThread( + () -> ShortcutsUpdater.updatePinnedShortcuts(mContext)); mLocalesToSetNext = null; diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java deleted file mode 100644 index 45d475f8eb8..00000000000 --- a/src/com/android/settings/network/MobileNetworkSummaryController.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2019 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.network; - -import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; -import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; - -import android.content.Context; -import android.content.Intent; -import android.telephony.SubscriptionManager; -import android.telephony.euicc.EuiccManager; - -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.OnLifecycleEvent; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.network.telephony.SimRepository; -import com.android.settings.network.telephony.euicc.EuiccRepository; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.RestrictedPreference; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.mobile.dataservice.MobileNetworkInfoEntity; -import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity; -import com.android.settingslib.mobile.dataservice.UiccInfoEntity; - -import java.util.List; -import java.util.stream.Collectors; - -public class MobileNetworkSummaryController extends AbstractPreferenceController implements - LifecycleObserver, PreferenceControllerMixin, - MobileNetworkRepository.MobileNetworkCallback { - private static final String TAG = "MobileNetSummaryCtlr"; - - private static final String KEY = "mobile_network_list"; - - private final MetricsFeatureProvider mMetricsFeatureProvider; - private RestrictedPreference mPreference; - - private MobileNetworkRepository mMobileNetworkRepository; - private List mSubInfoEntityList; - private List mUiccInfoEntityList; - private List mMobileNetworkInfoEntityList; - private boolean mIsAirplaneModeOn; - private LifecycleOwner mLifecycleOwner; - - /** - * This controls the summary text and click behavior of the "Mobile network" item on the - * Network & internet page. There are 3 separate cases depending on the number of mobile network - * subscriptions: - *
    - *
  • No subscription: click action begins a UI flow to add a network subscription, and - * the summary text indicates this
  • - * - *
  • One subscription: click action takes you to details for that one network, and - * the summary text is the network name
  • - * - *
  • More than one subscription: click action takes you to a page listing the subscriptions, - * and the summary text gives the count of SIMs
  • - *
- */ - public MobileNetworkSummaryController(Context context, Lifecycle lifecycle, - LifecycleOwner lifecycleOwner) { - super(context); - mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); - mLifecycleOwner = lifecycleOwner; - mMobileNetworkRepository = MobileNetworkRepository.getInstance(context); - mIsAirplaneModeOn = mMobileNetworkRepository.isAirplaneModeOn(); - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } - - @OnLifecycleEvent(ON_RESUME) - public void onResume() { - mMobileNetworkRepository.addRegister(mLifecycleOwner, this, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - mMobileNetworkRepository.updateEntity(); - } - - @OnLifecycleEvent(ON_PAUSE) - public void onPause() { - mMobileNetworkRepository.removeRegister(this); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); - } - - @Override - public CharSequence getSummary() { - - if ((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) || ( - mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || ( - mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty())) { - if (new EuiccRepository(mContext).showEuiccSettings()) { - return mContext.getResources().getString( - R.string.mobile_network_summary_add_a_network); - } - // set empty string to override previous text for carrier when SIM available - return ""; - } else if (mSubInfoEntityList.size() == 1) { - SubscriptionInfoEntity info = mSubInfoEntityList.get(0); - CharSequence displayName = info.uniqueName; - if (info.isEmbedded || mUiccInfoEntityList.get(0).isActive - || mMobileNetworkInfoEntityList.get(0).showToggleForPhysicalSim) { - return displayName; - } - return mContext.getString(R.string.mobile_network_tap_to_activate, displayName); - } else { - return mSubInfoEntityList.stream() - .map(SubscriptionInfoEntity::getUniqueDisplayName) - .collect(Collectors.joining(", ")); - } - } - - private void logPreferenceClick(Preference preference) { - mMetricsFeatureProvider.logClickedPreference(preference, - preference.getExtras().getInt(DashboardFragment.CATEGORY)); - } - - private void startAddSimFlow() { - final Intent intent = new Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); - intent.setPackage(com.android.settings.Utils.PHONE_PACKAGE_NAME); - intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true); - mContext.startActivity(intent); - } - - private void initPreference() { - refreshSummary(mPreference); - mPreference.setOnPreferenceClickListener(null); - mPreference.setFragment(null); - mPreference.setEnabled(!mIsAirplaneModeOn); - } - - private void update() { - if (mPreference == null || mPreference.isDisabledByAdmin()) { - return; - } - - initPreference(); - if (((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) - || (mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) - || (mMobileNetworkInfoEntityList == null - || mMobileNetworkInfoEntityList.isEmpty()))) { - if (new EuiccRepository(mContext).showEuiccSettings()) { - mPreference.setOnPreferenceClickListener((Preference pref) -> { - logPreferenceClick(pref); - startAddSimFlow(); - return true; - }); - } else { - mPreference.setEnabled(false); - } - return; - } - - mPreference.setFragment(MobileNetworkListFragment.class.getCanonicalName()); - } - - @Override - public boolean isAvailable() { - return new SimRepository(mContext).showMobileNetworkPage(); - } - - @Override - public String getPreferenceKey() { - return KEY; - } - - @Override - public void onAirplaneModeChanged(boolean airplaneModeEnabled) { - if (mIsAirplaneModeOn != airplaneModeEnabled) { - mIsAirplaneModeOn = airplaneModeEnabled; - update(); - } - } - - @Override - public void onAvailableSubInfoChanged(List subInfoEntityList) { - mSubInfoEntityList = subInfoEntityList; - update(); - } - - @Override - public void onAllUiccInfoChanged(List uiccInfoEntityList) { - mUiccInfoEntityList = uiccInfoEntityList; - update(); - } - - @Override - public void onAllMobileNetworkInfoChanged( - List mobileNetworkInfoEntityList) { - mMobileNetworkInfoEntityList = mobileNetworkInfoEntityList; - update(); - } -} diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.kt b/src/com/android/settings/network/MobileNetworkSummaryController.kt new file mode 100644 index 00000000000..5980bbd7d05 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkSummaryController.kt @@ -0,0 +1,121 @@ +/* + * 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.network + +import android.content.Context +import android.provider.Settings +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.network.telephony.SimRepository +import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settings.spa.network.startAddSimFlow +import com.android.settingslib.RestrictedPreference +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow +import kotlinx.coroutines.flow.Flow + +/** + * This controls the summary text and click behavior of the "Mobile network" item on the Network & + * internet page. There are 2 separate cases depending on the number of mobile network + * subscriptions: + * - No subscription: click action begins a UI flow to add a network subscription, and the summary + * text indicates this + * - Has subscriptions: click action takes you to a page listing the subscriptions, and the summary + * text gives the count of SIMs + */ +class MobileNetworkSummaryController +@JvmOverloads +constructor( + private val context: Context, + preferenceKey: String, + private val repository: MobileNetworkSummaryRepository = + MobileNetworkSummaryRepository(context), + private val airplaneModeOnFlow: Flow = + context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON), +) : BasePreferenceController(context, preferenceKey) { + private val metricsFeatureProvider = featureFactory.metricsFeatureProvider + private var preference: RestrictedPreference? = null + + private var isAirplaneModeOn = false + + override fun getAvailabilityStatus() = + if (SimRepository(mContext).showMobileNetworkPage()) AVAILABLE + else CONDITIONALLY_UNAVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + repository + .subscriptionsStateFlow() + .collectLatestWithLifecycle(viewLifecycleOwner, action = ::update) + airplaneModeOnFlow.collectLatestWithLifecycle(viewLifecycleOwner) { + isAirplaneModeOn = it + updateEnabled() + } + } + + private fun update(state: MobileNetworkSummaryRepository.SubscriptionsState) { + val preference = preference ?: return + preference.onPreferenceClickListener = null + preference.fragment = null + when (state) { + MobileNetworkSummaryRepository.AddNetwork -> { + preference.summary = + context.getString(R.string.mobile_network_summary_add_a_network) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + logPreferenceClick() + startAddSimFlow(context) + true + } + } + + MobileNetworkSummaryRepository.NoSubscriptions -> { + preference.summary = null + } + + is MobileNetworkSummaryRepository.HasSubscriptions -> { + preference.summary = state.displayNames.joinToString(", ") + preference.fragment = MobileNetworkListFragment::class.java.canonicalName + } + } + updateEnabled() + } + + private fun updateEnabled() { + val preference = preference ?: return + if (preference.isDisabledByAdmin) return + preference.isEnabled = + (preference.onPreferenceClickListener != null || preference.fragment != null) && + !isAirplaneModeOn + } + + private fun logPreferenceClick() { + val preference = preference ?: return + metricsFeatureProvider.logClickedPreference( + preference, + preference.extras.getInt(DashboardFragment.CATEGORY), + ) + } +} diff --git a/src/com/android/settings/network/MobileNetworkSummaryRepository.kt b/src/com/android/settings/network/MobileNetworkSummaryRepository.kt new file mode 100644 index 00000000000..edf557bb725 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkSummaryRepository.kt @@ -0,0 +1,66 @@ +/* + * 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.network + +import android.content.Context +import android.telephony.SubscriptionInfo +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class MobileNetworkSummaryRepository( + private val context: Context, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), + private val euiccRepository: EuiccRepository = EuiccRepository(context), + private val getDisplayName: (SubscriptionInfo) -> String = { subInfo -> + SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context).toString() + }, +) { + sealed interface SubscriptionsState + + data object AddNetwork : SubscriptionsState + + data object NoSubscriptions : SubscriptionsState + + data class HasSubscriptions(val displayNames: List) : SubscriptionsState + + fun subscriptionsStateFlow(): Flow = + subDisplayNamesFlow() + .map { displayNames -> + if (displayNames.isEmpty()) { + if (euiccRepository.showEuiccSettings()) AddNetwork else NoSubscriptions + } else { + HasSubscriptions(displayNames) + } + } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) + + private fun subDisplayNamesFlow(): Flow> = + subscriptionRepository + .selectableSubscriptionInfoListFlow() + .map { subInfos -> subInfos.map(getDisplayName) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) +} diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index aff91308e94..ee7d440bcf3 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -19,7 +19,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import androidx.lifecycle.LifecycleOwner; +import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settings.SettingsDumpService; @@ -69,12 +69,11 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override protected List createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getSettingsLifecycle(), - this /* LifecycleOwner */); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List buildPreferenceControllers(Context context, - Lifecycle lifecycle, LifecycleOwner lifecycleOwner) { + @Nullable Lifecycle lifecycle) { final VpnPreferenceController vpnPreferenceController = new VpnPreferenceController(context); final PrivateDnsPreferenceController privateDnsPreferenceController = @@ -87,7 +86,6 @@ public class NetworkDashboardFragment extends DashboardFragment implements final List controllers = new ArrayList<>(); - controllers.add(new MobileNetworkSummaryController(context, lifecycle, lifecycleOwner)); controllers.add(vpnPreferenceController); controllers.add(privateDnsPreferenceController); @@ -114,8 +112,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override public List createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, null /* lifecycle */, - null /* LifecycleOwner */); + return buildPreferenceControllers(context, null /* lifecycle */); } }; } diff --git a/src/com/android/settings/network/telephony/VoNrRepository.kt b/src/com/android/settings/network/telephony/VoNrRepository.kt index 635c57223be..7f3823b6481 100644 --- a/src/com/android/settings/network/telephony/VoNrRepository.kt +++ b/src/com/android/settings/network/telephony/VoNrRepository.kt @@ -19,6 +19,7 @@ package com.android.settings.network.telephony import android.content.Context import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -37,12 +38,13 @@ class VoNrRepository( fun isVoNrAvailable(subId: Int): Boolean { if (!nrRepository.isNrAvailable(subId)) return false data class Config(val isVoNrEnabled: Boolean, val isVoNrSettingVisibility: Boolean) + val carrierConfig = carrierConfigRepository.transformConfig(subId) { Config( isVoNrEnabled = getBoolean(CarrierConfigManager.KEY_VONR_ENABLED_BOOL), isVoNrSettingVisibility = - getBoolean(CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL), + getBoolean(CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL), ) } return carrierConfig.isVoNrEnabled && carrierConfig.isVoNrSettingVisibility @@ -52,7 +54,14 @@ class VoNrRepository( val telephonyManager = context.telephonyManager(subId) return context .subscriptionsChangedFlow() - .map { telephonyManager.isVoNrEnabled } + .map { + try { + telephonyManager.isVoNrEnabled + } catch (e: IllegalStateException) { + Log.e(TAG, "IllegalStateException - isVoNrEnabled : $e") + false + } + } .conflate() .onEach { Log.d(TAG, "[$subId] isVoNrEnabled: $it") } .flowOn(Dispatchers.Default) @@ -61,11 +70,17 @@ class VoNrRepository( suspend fun setVoNrEnabled(subId: Int, enabled: Boolean) = withContext(Dispatchers.Default) { if (!SubscriptionManager.isValidSubscriptionId(subId)) return@withContext - val result = context.telephonyManager(subId).setVoNrEnabled(enabled) - Log.d(TAG, "[$subId] setVoNrEnabled: $enabled, result: $result") + var result = TelephonyManager.ENABLE_VONR_RADIO_INVALID_STATE + try { + result = context.telephonyManager(subId).setVoNrEnabled(enabled) + } catch (e: IllegalStateException) { + Log.e(TAG, "IllegalStateException - setVoNrEnabled : $e") + } finally { + Log.d(TAG, "[$subId] setVoNrEnabled: $enabled, result: $result") + } } private companion object { private const val TAG = "VoNrRepository" } -} +} \ No newline at end of file diff --git a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java index 00f478f2d28..946b7837995 100644 --- a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java +++ b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.notification; import android.content.Context; import android.media.RingtoneManager; +import com.android.server.notification.Flags; import com.android.settings.R; public class NotificationRingtonePreferenceController extends RingtonePreferenceControllerBase { @@ -31,6 +32,9 @@ public class NotificationRingtonePreferenceController extends RingtonePreference @Override public boolean isAvailable() { + if (isVibrationInSoundUriEnabled()) { + return false; + } return mContext.getResources().getBoolean(R.bool.config_show_notification_ringtone); } @@ -43,4 +47,9 @@ public class NotificationRingtonePreferenceController extends RingtonePreference public int getRingtoneType() { return RingtoneManager.TYPE_NOTIFICATION; } + + private boolean isVibrationInSoundUriEnabled() { + return Flags.notificationVibrationInSoundUri() && mContext.getResources().getBoolean( + com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported); + } } diff --git a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java index 8f74bd9e05a..0e2e6bcb44c 100644 --- a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java +++ b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java @@ -16,27 +16,19 @@ package com.android.settings.shortcut; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; import android.net.ConnectivityManager; import android.util.Log; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -48,7 +40,6 @@ import com.android.settings.Settings; import com.android.settings.Settings.DataUsageSummaryActivity; import com.android.settings.Settings.TetherSettingsActivity; import com.android.settings.Settings.WifiTetherSettingsActivity; -import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.core.BasePreferenceController; import com.android.settings.gestures.OneHandedSettingsUtils; import com.android.settings.network.SubscriptionUtil; @@ -69,11 +60,6 @@ public class CreateShortcutPreferenceController extends BasePreferenceController private static final String TAG = "CreateShortcutPrefCtrl"; - static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; - static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN) - .addCategory("com.android.settings.SHORTCUT") - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - private final ShortcutManager mShortcutManager; private final PackageManager mPackageManager; private final ConnectivityManager mConnectivityManager; @@ -132,9 +118,7 @@ public class CreateShortcutPreferenceController extends BasePreferenceController if (mHost == null) { return false; } - final Intent shortcutIntent = createResultIntent( - buildShortcutIntent(uiContext, info), - info, clickTarget.getTitle()); + final Intent shortcutIntent = createResultIntent(info); mHost.setResult(Activity.RESULT_OK, shortcutIntent); logCreateShortcut(info); mHost.finish(); @@ -149,21 +133,20 @@ public class CreateShortcutPreferenceController extends BasePreferenceController * launcher widget using this intent. */ @VisibleForTesting - Intent createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo, - CharSequence label) { - ShortcutInfo info = createShortcutInfo(mContext, shortcutIntent, resolveInfo, label); + Intent createResultIntent(ResolveInfo resolveInfo) { + ShortcutInfo info = Shortcuts.createShortcutInfo(mContext, resolveInfo); Intent intent = mShortcutManager.createShortcutResultIntent(info); if (intent == null) { intent = new Intent(); } intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(mContext, R.mipmap.ic_launcher_settings)) - .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) - .putExtra(Intent.EXTRA_SHORTCUT_NAME, label); + .putExtra(Intent.EXTRA_SHORTCUT_INTENT, info.getIntent()) + .putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getShortLabel()); final ActivityInfo activityInfo = resolveInfo.activityInfo; if (activityInfo.icon != 0) { - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon( + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, Shortcuts.createIcon( mContext, activityInfo.applicationInfo, activityInfo.icon, @@ -239,87 +222,6 @@ public class CreateShortcutPreferenceController extends BasePreferenceController info.activityInfo.name); } - private static Intent buildShortcutIntent(Context context, ResolveInfo info) { - Intent intent = new Intent(SHORTCUT_PROBE) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) - .setClassName(info.activityInfo.packageName, info.activityInfo.name); - if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) { - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - return intent; - } - - private static ShortcutInfo createShortcutInfo(Context context, Intent shortcutIntent, - ResolveInfo resolveInfo, CharSequence label) { - final ActivityInfo activityInfo = resolveInfo.activityInfo; - - final Icon maskableIcon; - if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) { - maskableIcon = Icon.createWithAdaptiveBitmap(createIcon( - context, - activityInfo.applicationInfo, activityInfo.icon, - R.layout.shortcut_badge_maskable, - context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))); - } else { - maskableIcon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); - } - final String shortcutId = SHORTCUT_ID_PREFIX + - shortcutIntent.getComponent().flattenToShortString(); - return new ShortcutInfo.Builder(context, shortcutId) - .setShortLabel(label) - .setIntent(shortcutIntent) - .setIcon(maskableIcon) - .build(); - } - - private static Bitmap createIcon(Context context, ApplicationInfo app, int resource, - int layoutRes, int size) { - final Context themedContext = new ContextThemeWrapper(context, - android.R.style.Theme_Material); - final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null); - final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY); - view.measure(spec, spec); - final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), - Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(bitmap); - - Drawable iconDrawable; - try { - iconDrawable = context.getPackageManager().getResourcesForApplication(app) - .getDrawable(resource, themedContext.getTheme()); - if (iconDrawable instanceof LayerDrawable) { - iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); - } - ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon"); - Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); - ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon); - } - - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - view.draw(canvas); - return bitmap; - } - - public static void updateRestoredShortcuts(Context context) { - ShortcutManager sm = context.getSystemService(ShortcutManager.class); - List updatedShortcuts = new ArrayList<>(); - for (ShortcutInfo si : sm.getPinnedShortcuts()) { - if (si.getId().startsWith(SHORTCUT_ID_PREFIX)) { - ResolveInfo ri = context.getPackageManager().resolveActivity(si.getIntent(), 0); - - if (ri != null) { - updatedShortcuts.add(createShortcutInfo(context, - buildShortcutIntent(context, ri), ri, si.getShortLabel())); - } - } - } - if (!updatedShortcuts.isEmpty()) { - sm.updateShortcuts(updatedShortcuts); - } - } - private static final Comparator SHORTCUT_COMPARATOR = (i1, i2) -> i1.priority - i2.priority; } diff --git a/src/com/android/settings/shortcut/Shortcuts.java b/src/com/android/settings/shortcut/Shortcuts.java new file mode 100644 index 00000000000..53544ebe120 --- /dev/null +++ b/src/com/android/settings/shortcut/Shortcuts.java @@ -0,0 +1,118 @@ +/* + * 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.shortcut; + +import static com.google.common.base.Preconditions.checkArgument; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.LayerDrawable; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.activityembedding.ActivityEmbeddingUtils; + +class Shortcuts { + + private static final String TAG = "Shortcuts"; + + static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; + static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN) + .addCategory("com.android.settings.SHORTCUT") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + static ShortcutInfo createShortcutInfo(Context context, ResolveInfo target) { + checkArgument(target.activityInfo != null); + String shortcutId = SHORTCUT_ID_PREFIX + + target.activityInfo.getComponentName().flattenToShortString(); + + return createShortcutInfo(context, shortcutId, target); + } + + static ShortcutInfo createShortcutInfo(Context context, String id, ResolveInfo target) { + Intent intent = new Intent(SHORTCUT_PROBE) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setClassName(target.activityInfo.packageName, target.activityInfo.name); + if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) { + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + CharSequence label = target.loadLabel(context.getPackageManager()); + Icon maskableIcon = getMaskableIcon(context, target.activityInfo); + + return new ShortcutInfo.Builder(context, id) + .setIntent(intent) + .setShortLabel(label) + .setIcon(maskableIcon) + .build(); + } + + private static Icon getMaskableIcon(Context context, ActivityInfo activityInfo) { + if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) { + return Icon.createWithAdaptiveBitmap(createIcon( + context, + activityInfo.applicationInfo, activityInfo.icon, + R.layout.shortcut_badge_maskable, + context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))); + } else { + return Icon.createWithResource(context, R.drawable.ic_launcher_settings); + } + } + + static Bitmap createIcon(Context context, ApplicationInfo app, int resource, int layoutRes, + int size) { + final Context themedContext = new ContextThemeWrapper(context, + android.R.style.Theme_Material); + final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null); + final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY); + view.measure(spec, spec); + final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + Drawable iconDrawable; + try { + iconDrawable = context.getPackageManager().getResourcesForApplication(app) + .getDrawable(resource, themedContext.getTheme()); + if (iconDrawable instanceof LayerDrawable) { + iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); + } + ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon"); + Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); + ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon); + } + + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + view.draw(canvas); + return bitmap; + } +} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java b/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java new file mode 100644 index 00000000000..657af5ba027 --- /dev/null +++ b/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java @@ -0,0 +1,53 @@ +/* + * 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.shortcut; + +import android.app.Flags; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.settingslib.utils.ThreadUtils; + +public class ShortcutsUpdateReceiver extends BroadcastReceiver { + + private static final String TAG = "ShortcutsUpdateReceiver"; + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + if (!Flags.modesApi() || !Flags.modesUi()) { + return; + } + + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + PendingResult pendingResult = goAsync(); + + ThreadUtils.getBackgroundExecutor().execute(() -> { + try { + ShortcutsUpdater.updatePinnedShortcuts(context); + } catch (Exception e) { + Log.e(TAG, "Error trying to update Settings shortcuts", e); + } finally { + pendingResult.finish(); + } + }); + } + } +} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdateTask.java b/src/com/android/settings/shortcut/ShortcutsUpdateTask.java deleted file mode 100644 index 54f7d1ceafe..00000000000 --- a/src/com/android/settings/shortcut/ShortcutsUpdateTask.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 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.shortcut; - -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_PROBE; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.os.AsyncTask; - -import java.util.ArrayList; -import java.util.List; - -public class ShortcutsUpdateTask extends AsyncTask { - - private final Context mContext; - - public ShortcutsUpdateTask(Context context) { - mContext = context; - } - - @Override - public Void doInBackground(Void... params) { - ShortcutManager sm = mContext.getSystemService(ShortcutManager.class); - PackageManager pm = mContext.getPackageManager(); - - List updates = new ArrayList<>(); - for (ShortcutInfo info : sm.getPinnedShortcuts()) { - if (!info.getId().startsWith(SHORTCUT_ID_PREFIX)) { - continue; - } - ComponentName cn = ComponentName.unflattenFromString( - info.getId().substring(SHORTCUT_ID_PREFIX.length())); - ResolveInfo ri = pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0); - if (ri == null) { - continue; - } - updates.add(new ShortcutInfo.Builder(mContext, info.getId()) - .setShortLabel(ri.loadLabel(pm)).build()); - } - if (!updates.isEmpty()) { - sm.updateShortcuts(updates); - } - return null; - } -} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdater.java b/src/com/android/settings/shortcut/ShortcutsUpdater.java new file mode 100644 index 00000000000..90a60fda379 --- /dev/null +++ b/src/com/android/settings/shortcut/ShortcutsUpdater.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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.shortcut; + +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.app.Flags; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.Settings; + +import java.util.ArrayList; +import java.util.List; + +public class ShortcutsUpdater { + + /** + * Update label, icon, and intent of pinned shortcuts to Settings subpages. + * + *

Should be called whenever any of those could have changed, such as after changing locale, + * restoring a backup from a different device, or when flags controlling available features + * may have flipped. + */ + public static void updatePinnedShortcuts(Context context) { + ShortcutManager sm = checkNotNull(context.getSystemService(ShortcutManager.class)); + + List updates = new ArrayList<>(); + for (ShortcutInfo info : sm.getPinnedShortcuts()) { + ResolveInfo resolvedActivity = resolveActivity(context, info); + if (resolvedActivity != null) { + // Id is preserved to update an existing shortcut, but the activity it opens might + // be different, according to maybeGetReplacingComponent. + updates.add(Shortcuts.createShortcutInfo(context, info.getId(), resolvedActivity)); + } + } + if (!updates.isEmpty()) { + sm.updateShortcuts(updates); + } + } + + @Nullable + private static ResolveInfo resolveActivity(Context context, ShortcutInfo shortcut) { + if (!shortcut.getId().startsWith(SHORTCUT_ID_PREFIX)) { + return null; + } + + ComponentName cn = ComponentName.unflattenFromString( + shortcut.getId().substring(SHORTCUT_ID_PREFIX.length())); + if (cn == null) { + return null; + } + + // Check if the componentName is obsolete and has been replaced by a different one. + cn = maybeGetReplacingComponent(context, cn); + PackageManager pm = context.getPackageManager(); + return pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0); + } + + @NonNull + private static ComponentName maybeGetReplacingComponent(Context context, ComponentName cn) { + // ZenModeSettingsActivity is replaced by ModesSettingsActivity and will be deleted + // soon (so we shouldn't use ZenModeSettingsActivity.class). + if (Flags.modesApi() && Flags.modesUi() + && cn.getClassName().endsWith("Settings$ZenModeSettingsActivity")) { + return new ComponentName(context, Settings.ModesSettingsActivity.class); + } + + return cn; + } +} diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 276d121c24f..bd55b32a5e8 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -137,7 +137,7 @@ private fun AddSim() { } } -private fun startAddSimFlow(context: Context) { +fun startAddSimFlow(context: Context) { val intent = Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION) intent.setPackage(Utils.PHONE_PACKAGE_NAME) intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true) diff --git a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java index 14ef4833588..00bd0f23cdc 100644 --- a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java +++ b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java @@ -41,19 +41,20 @@ public class WallpaperSuggestionActivity extends StyleSuggestionActivityBase imp private static final String WALLPAPER_FOCUS = "focus_wallpaper"; private static final String WALLPAPER_ONLY = "wallpaper_only"; private static final String LAUNCHED_SUW = "app_launched_suw"; - - private String mWallpaperLaunchExtra; + private static final String LAUNCH_SOURCE_SETTINGS_SEARCH = "app_launched_settings_search"; @Override protected void addExtras(Intent intent) { + String wallpaperLaunchExtra = + getResources().getString(R.string.config_wallpaper_picker_launch_extra);; if (WizardManagerHelper.isAnySetupWizard(intent)) { intent.putExtra(WALLPAPER_FLAVOR_EXTRA, WALLPAPER_ONLY); - - mWallpaperLaunchExtra = - getResources().getString(R.string.config_wallpaper_picker_launch_extra); - intent.putExtra(mWallpaperLaunchExtra, LAUNCHED_SUW); + intent.putExtra(wallpaperLaunchExtra, LAUNCHED_SUW); } else { + // This is the case when user enter the wallpaper picker from the search result entry + // on the Settings app intent.putExtra(WALLPAPER_FLAVOR_EXTRA, WALLPAPER_FOCUS); + intent.putExtra(wallpaperLaunchExtra, LAUNCH_SOURCE_SETTINGS_SEARCH); } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 40f7895d8dc..e326c1e2630 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -16,52 +16,80 @@ package com.android.settings.bluetooth; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; 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 static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import java.util.concurrent.Executor; + /** Tests for {@link BluetoothDevicePairingDetailBase}. */ @RunWith(RobolectricTestRunner.class) @Config(shadows = { ShadowBluetoothAdapter.class, + ShadowAlertDialogCompat.class, com.android.settings.testutils.shadow.ShadowFragment.class, }) public class BluetoothDevicePairingDetailBaseTest { @Rule - public final MockitoRule mockito = MockitoJUnit.rule(); + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); public static final String KEY_DEVICE_LIST_GROUP = "test_key"; @@ -86,8 +114,12 @@ public class BluetoothDevicePairingDetailBaseTest { @Before public void setUp() { mAvailableDevicesCategory = spy(new BluetoothProgressCategory(mContext)); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); final Pair pairs = new Pair<>(mDrawable, "fake_device"); when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pairs); @@ -155,8 +187,88 @@ public class BluetoothDevicePairingDetailBaseTest { verify(mFragment).showBluetoothTurnedOnToast(); } + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingDisabled_finish() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + + verify(mFragment).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + TextView message = dialog.findViewById(R.id.message); + assertThat(message).isNotNull(); + // TODO: use stringr res once finalized + assertThat(message.getText().toString()).isEqualTo( + "Connecting to " + TEST_DEVICE_ADDRESS + "..."); + verify(mFragment, never()).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter, never()).addOnMetadataChangedListener(any(BluetoothDevice.class), + any(Executor.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingEnabled_addListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), + any(Executor.class), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter, never()).removeOnMetadataChangedListener( + any(BluetoothDevice.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingEnabled_removeListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter).removeOnMetadataChangedListener(eq(mBluetoothDevice), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + @Test public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_finish() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -165,13 +277,43 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); verify(mFragment).finish(); } + @Test + public void + onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + when(mCachedBluetoothDevice.isConnected()).thenReturn(true); + + mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment.getActivity()).setResult(eq(Activity.RESULT_OK), captor.capture()); + Intent intent = captor.getValue(); + BluetoothDevice btDevice = + intent != null + ? intent.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + assertThat(btDevice).isNotNull(); + assertThat(btDevice).isEqualTo(mBluetoothDevice); + verify(mFragment).finish(); + } + @Test public void onProfileConnectionStateChanged_deviceNotInSelectedList_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(device); @@ -179,13 +321,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceDisconnected_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -194,13 +337,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_DISCONNECTED); + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceInPreferenceMapAndConnected_removed() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); @@ -211,13 +355,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0); } @Test public void onProfileConnectionStateChanged_deviceNotInPreferenceMap_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, @@ -233,12 +378,26 @@ public class BluetoothDevicePairingDetailBaseTest { when(cachedDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); when(cachedDevice.getIdentityAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); - mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothProfile.A2DP, - BluetoothAdapter.STATE_CONNECTED); + mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); // not crash } + private void setUpFragmentWithPairAndJoinSharingIntent(boolean enablePairAndJoinSharing) { + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enablePairAndJoinSharing); + Intent intent = new Intent(); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); + doReturn(intent).when(activity).getIntent(); + doReturn(activity).when(mFragment).getActivity(); + FragmentManager fragmentManager = mock(FragmentManager.class); + doReturn(fragmentManager).when(mFragment).getFragmentManager(); + mFragment.mShouldTriggerAudioSharingShareThenPairFlow = + mFragment.shouldTriggerAudioSharingShareThenPairFlow(); + } + private static class TestBluetoothDevicePairingDetailBase extends BluetoothDevicePairingDetailBase { diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java index 7d8846d0506..1ce3316811b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java @@ -16,16 +16,29 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +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 static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.View; import androidx.test.core.app.ApplicationProvider; @@ -33,24 +46,29 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowFragment.class}) +@Config(shadows = {ShadowFragment.class, ShadowBluetoothAdapter.class}) public class AudioSharingDashboardFragmentTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private SettingsActivity mActivity; @Mock private SettingsMainSwitchBar mSwitchBar; @@ -59,11 +77,19 @@ public class AudioSharingDashboardFragmentTest { @Mock private AudioSharingCallAudioPreferenceController mCallAudioController; @Mock private AudioSharingPlaySoundPreferenceController mPlaySoundController; @Mock private AudioStreamsCategoryController mStreamsCategoryController; + @Mock private AudioSharingSwitchBarController mSwitchBarController; private final Context mContext = ApplicationProvider.getApplicationContext(); private AudioSharingDashboardFragment mFragment; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; @Before public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mSwitchBar.getRootView()).thenReturn(mView); mFragment = new AudioSharingDashboardFragment(); } @@ -100,13 +126,73 @@ public class AudioSharingDashboardFragmentTest { verify(mSwitchBar).show(); } + @Test + public void onActivityResult_shareThenPairWithBadCode_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(device); + } + + @Test + public void onActivityResult_shareThenPairWithNoDevice_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, null); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(any()); + } + + @Test + public void onActivityResult_shareThenPairWithDevice_handleAutoAddSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_OK, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController).handleAutoAddSourceAfterPair(device); + } + @Test public void onAudioSharingStateChanged_updateVisibilityForControllers() { mFragment.setControllers( mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingStateChanged(); verify(mVolumeGroupController).updateVisibility(); verify(mCallAudioController).updateVisibility(); @@ -120,7 +206,8 @@ public class AudioSharingDashboardFragmentTest { mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingProfilesConnected(); verify(mVolumeGroupController).onAudioSharingProfilesConnected(); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java index 7227f37998b..dec85e47b80 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java @@ -34,6 +34,7 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -82,11 +83,6 @@ public class AudioSharingDialogFragmentTest { new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false); private static final AudioSharingDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER = new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - - @Override - public void onCancelClick() {} }; private static final Pair TEST_EVENT_DATA = Pair.create(1, 1); private static final Pair[] TEST_EVENT_DATA_LIST = @@ -176,8 +172,17 @@ public class AudioSharingDialogFragmentTest { @Test public void onCreateDialog_noExtraConnectedDevice_pairNewDevice() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isPairBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + isPairBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -191,14 +196,24 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isPairBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @Test public void onCreateDialog_noExtraConnectedDevice_showQRCode() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isQrCodeBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onCancelClick() { + isQrCodeBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -212,6 +227,7 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isQrCodeBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @@ -286,12 +302,9 @@ public class AudioSharingDialogFragmentTest { list, new AudioSharingDialogFragment.DialogEventListener() { @Override - public void onItemClick(AudioSharingDeviceItem item) { + public void onItemClick(@NonNull AudioSharingDeviceItem item) { isShareBtnClicked.set(true); } - - @Override - public void onCancelClick() {} }, TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); @@ -359,9 +372,6 @@ public class AudioSharingDialogFragmentTest { mParent, list, new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - @Override public void onCancelClick() { isCancelBtnClicked.set(true); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java index 7f172918d36..67cb2aa915b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java @@ -18,7 +18,6 @@ package com.android.settings.connecteddevice.audiosharing; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.bluetooth.BluetoothAdapter; @@ -34,7 +33,6 @@ import androidx.fragment.app.FragmentActivity; import com.android.settings.R; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.flags.Flags; import org.junit.After; @@ -42,7 +40,6 @@ import org.junit.Before; 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; @@ -62,7 +59,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private CachedBluetoothDevice mCachedBluetoothDevice; private Fragment mParent; private AudioSharingIncompatibleDialogFragment mFragment; @@ -76,7 +72,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { BluetoothStatusCodes.FEATURE_SUPPORTED); shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( BluetoothStatusCodes.FEATURE_SUPPORTED); - when(mCachedBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME); mFragment = new AudioSharingIncompatibleDialogFragment(); mParent = new Fragment(); FragmentController.setupFragment(mParent, FragmentActivity.class, /* containerViewId= */ @@ -97,7 +92,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOff_dialogNotExist() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -107,7 +102,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_unattachedFragment_dialogNotExist() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(new Fragment(), mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(new Fragment(), TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -117,7 +112,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOn_showDialog() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -134,7 +129,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { public void onCreateDialog_clickBtn_callbackTriggered() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); AtomicBoolean isBtnClicked = new AtomicBoolean(false); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, () -> isBtnClicked.set(true)); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java index 0d21f18b821..eb2083ebe7b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java @@ -934,6 +934,19 @@ public class AudioSharingSwitchBarControllerTest { childFragments.forEach(fragment -> ((DialogFragment) fragment).dismiss()); } + @Test + public void handleAutoAddSourceAfterPair() { + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + mController.handleAutoAddSourceAfterPair(mDevice1); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).comparingElementsUsing(CLAZZNAME_EQUALS).containsExactly( + AudioSharingLoadingStateDialogFragment.class.getName()); + } + private Fragment setUpFragmentWithStartSharingIntent() { Bundle args = new Bundle(); args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true); diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java deleted file mode 100644 index 1823d6d6bed..00000000000 --- a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2019 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.network; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -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.content.Context; -import android.content.Intent; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.telephony.euicc.EuiccManager; -import android.text.TextUtils; - -import androidx.lifecycle.LifecycleOwner; -import androidx.preference.PreferenceScreen; - -import com.android.settings.Settings.MobileNetworkActivity; -import com.android.settings.widget.AddPreference; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.core.lifecycle.Lifecycle; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -import java.util.Arrays; - -@RunWith(RobolectricTestRunner.class) -public class MobileNetworkSummaryControllerTest { - - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private EuiccManager mEuiccManager; - @Mock - private PreferenceScreen mPreferenceScreen; - @Mock - private MobileNetworkRepository mMobileNetworkRepository; - @Mock - private MobileNetworkRepository.MobileNetworkCallback mMobileNetworkCallback; - - private AddPreference mPreference; - private Context mContext; - private MobileNetworkSummaryController mController; - private LifecycleOwner mLifecycleOwner; - private Lifecycle mLifecycle; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class); - doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class); - doReturn(mEuiccManager).when(mContext).getSystemService(EuiccManager.class); - mMobileNetworkRepository = MobileNetworkRepository.getInstance(mContext); - mLifecycleOwner = () -> mLifecycle; - mLifecycle = new Lifecycle(mLifecycleOwner); - mMobileNetworkRepository.addRegister(mLifecycleOwner, mMobileNetworkCallback, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - - when(mTelephonyManager.getNetworkCountryIso()).thenReturn(""); - when(mSubscriptionManager.isActiveSubscriptionId(anyInt())).thenReturn(true); - when(mEuiccManager.isEnabled()).thenReturn(true); - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.EUICC_PROVISIONED, 1); - - mController = new MobileNetworkSummaryController(mContext, mLifecycle, mLifecycleOwner); - mPreference = spy(new AddPreference(mContext, null)); - mPreference.setKey(mController.getPreferenceKey()); - when(mPreferenceScreen.findPreference(eq(mController.getPreferenceKey()))).thenReturn( - mPreference); - } - - @After - public void tearDown() { - mMobileNetworkRepository.removeRegister(mMobileNetworkCallback); - SubscriptionUtil.setActiveSubscriptionsForTesting(null); - SubscriptionUtil.setAvailableSubscriptionsForTesting(null); - } - - @Test - public void getSummary_noSubscriptions_returnSummaryCorrectly() { - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("Add a network"); - } - - @Test - public void getSummary_noSubscriptionsNoEuiccMgr_correctSummaryAndClickHandler() { - when(mEuiccManager.isEnabled()).thenReturn(false); - assertThat(TextUtils.isEmpty(mController.getSummary())).isTrue(); - assertThat(mPreference.getOnPreferenceClickListener()).isNull(); - assertThat(mPreference.getFragment()).isNull(); - } - - @Test - @Ignore - public void getSummary_oneSubscription_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - SubscriptionUtil.setActiveSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - assertThat(mController.getSummary()).isEqualTo("sub1"); - assertThat(mPreference.getFragment()).isNull(); - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - Intent intent = intentCaptor.getValue(); - assertThat(intent.getComponent().getClassName()).isEqualTo( - MobileNetworkActivity.class.getName()); - assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID)).isEqualTo(sub1.getSubscriptionId()); - } - - @Test - @Ignore - public void getSummary_oneInactivePSim_cannotDisablePsim_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - when(mSubscriptionManager.isActiveSubscriptionId(eq(1))).thenReturn(false); - - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("Tap to activate sub1"); - - assertThat(mPreference.getFragment()).isNull(); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - verify(mSubscriptionManager).setSubscriptionEnabled(eq(sub1.getSubscriptionId()), eq(true)); - } - - @Test - @Ignore - public void getSummary_oneInactivePSim_canDisablePsim_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - SubscriptionUtil.setActiveSubscriptionsForTesting(Arrays.asList(sub1)); - when(mSubscriptionManager.isActiveSubscriptionId(eq(1))).thenReturn(false); - when(mSubscriptionManager.canDisablePhysicalSubscription()).thenReturn(true); - - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("sub1"); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - Intent intent = intentCaptor.getValue(); - assertThat(intent.getComponent().getClassName()).isEqualTo( - MobileNetworkActivity.class.getName()); - assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID)).isEqualTo(sub1.getSubscriptionId()); - } - - @Test - public void addButton_noSubscriptionsNoEuiccMgr_noAddClickListener() { - when(mEuiccManager.isEnabled()).thenReturn(false); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - public void addButton_oneSubscriptionNoEuiccMgr_noAddClickListener() { - when(mEuiccManager.isEnabled()).thenReturn(false); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - public void addButton_noSubscriptions_noAddClickListener() { - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_oneSubscription_hasAddClickListener() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_twoSubscriptions_hasAddClickListener() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - final SubscriptionInfo sub2 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_oneSubscriptionAirplaneModeTurnedOn_addButtonGetsDisabled() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - mController.onAirplaneModeChanged(true); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isFalse(); - } - - @Test - @Ignore - public void onResume_oneSubscriptionAirplaneMode_isDisabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isFalse(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isFalse(); - } - - @Test - public void onAvailableSubInfoChanged_noSubscriptionEsimDisabled_isDisabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - when(mEuiccManager.isEnabled()).thenReturn(false); - mController.displayPreference(mPreferenceScreen); - - mController.onAvailableSubInfoChanged(null); - - assertThat(mPreference.isEnabled()).isFalse(); - } - - @Test - public void onAirplaneModeChanged_oneSubscriptionAirplaneModeGetsTurnedOn_isDisabled() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isTrue(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - mController.onAirplaneModeChanged(true); - - assertThat(mPreference.isEnabled()).isFalse(); - } - - @Test - @Ignore - public void onAirplaneModeChanged_oneSubscriptionAirplaneModeGetsTurnedOff_isEnabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isFalse(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - mController.onAirplaneModeChanged(false); - - assertThat(mPreference.isEnabled()).isTrue(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(eq(false)); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isTrue(); - } - - @Test - public void onResume_disabledByAdmin_prefStaysDisabled() { - mPreference.setDisabledByAdmin(new RestrictedLockUtils.EnforcedAdmin()); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setEnabled(eq(true)); - } -} diff --git a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java index 1aecad51242..a04a14de57b 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java @@ -18,35 +18,77 @@ package com.android.settings.notification; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; import android.media.RingtoneManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.server.notification.Flags; +import com.android.settings.R; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public class NotificationRingtonePreferenceControllerTest { private NotificationRingtonePreferenceController mController; + @Mock private Context mMockContext; + @Mock private Resources mMockResources; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new NotificationRingtonePreferenceController(RuntimeEnvironment.application); + when(mMockContext.getResources()).thenReturn(mMockResources); + mController = new NotificationRingtonePreferenceController(mMockContext); } @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) public void isAvailable_byDefault_isTrue() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(false); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(true); + assertThat(mController.isAvailable()).isTrue(); } @Test @Config(qualifiers = "mcc999") + @DisableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) public void isAvailable_whenNotVisible_isFalse() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(false); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(false); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) + public void isAvailable_whenFlagsNotificationVibrationInSoundUri_isFalse() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(true); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(true); + assertThat(mController.isAvailable()).isFalse(); } diff --git a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java index 9727dd13848..8442a37873b 100644 --- a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java @@ -16,7 +16,7 @@ package com.android.settings.shortcut; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; import static com.google.common.truth.Truth.assertThat; @@ -101,10 +101,10 @@ public class CreateShortcutPreferenceControllerTest { when(mShortcutManager.createShortcutResultIntent(any(ShortcutInfo.class))) .thenReturn(new Intent().putExtra("d1", "d2")); - final Intent intent = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) + final Intent intent = new Intent(Shortcuts.SHORTCUT_PROBE) .setClass(mContext, Settings.ManageApplicationsActivity.class); final ResolveInfo ri = mContext.getPackageManager().resolveActivity(intent, 0); - final Intent result = mController.createResultIntent(intent, ri, "mock"); + final Intent result = mController.createResultIntent(ri); assertThat(result.getStringExtra("d1")).isEqualTo("d2"); assertThat((Object) result.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT)).isNotNull(); @@ -131,7 +131,7 @@ public class CreateShortcutPreferenceControllerTest { ri2.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri1, ri2)); doReturn(false).when(mController).canShowWifiHotspot(); @@ -158,7 +158,7 @@ public class CreateShortcutPreferenceControllerTest { ri2.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri1, ri2)); doReturn(false).when(mController).canShowWifiHotspot(); @@ -276,7 +276,7 @@ public class CreateShortcutPreferenceControllerTest { ri.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri)); } } diff --git a/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java b/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java new file mode 100644 index 00000000000..a347ff9e0db --- /dev/null +++ b/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java @@ -0,0 +1,68 @@ +/* + * 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.shortcut; + +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; + +import com.android.settings.Settings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ShortcutsTest { + + private Context mContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); + } + + @Test + public void shortcutsUpdateTask() { + final Intent intent = new Intent(SHORTCUT_PROBE) + .setClass(mContext, Settings.ManageApplicationsActivity.class); + final ResolveInfo ri = mContext.getPackageManager().resolveActivity(intent, 0); + assertThat(ri).isNotNull(); + + ShortcutInfo shortcut = Shortcuts.createShortcutInfo(mContext, ri); + + assertThat(shortcut.getLabel()).isNotNull(); + assertThat(shortcut.getLabel().toString()).isEqualTo("App info"); + + assertThat(shortcut.getIntent()).isNotNull(); + assertThat(shortcut.getIntent().getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(shortcut.getIntent().getCategories()).contains("com.android.settings.SHORTCUT"); + assertThat(shortcut.getIntent().getComponent()).isEqualTo( + new ComponentName(mContext, Settings.ManageApplicationsActivity.class)); + assertThat(shortcut.getIcon()).isNotNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java similarity index 52% rename from tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java rename to tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java index 8352e7a9634..5324ff50f43 100644 --- a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java +++ b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java @@ -16,30 +16,30 @@ package com.android.settings.shortcut; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; 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.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Flags; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import com.android.settings.Settings; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -48,17 +48,17 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowPackageManager; import java.util.Arrays; import java.util.List; @RunWith(RobolectricTestRunner.class) -public class ShortcutsUpdateTaskTest { +public class ShortcutsUpdaterTest { private Context mContext; - private ShadowPackageManager mPackageManager; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private ShortcutManager mShortcutManager; @@ -68,29 +68,12 @@ public class ShortcutsUpdateTaskTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mPackageManager = Shadow.extract(mContext.getPackageManager()); + mContext = spy(RuntimeEnvironment.application); + doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE)); } @Test - public void shortcutsUpdateTask() { - mContext = spy(RuntimeEnvironment.application); - doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE)); - final Intent shortcut1 = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) - .setComponent(new ComponentName( - mContext, Settings.ManageApplicationsActivity.class)); - final ResolveInfo ri1 = mock(ResolveInfo.class); - ri1.nonLocalizedLabel = "label1"; - - final Intent shortcut2 = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) - .setComponent(new ComponentName( - mContext, Settings.SoundSettingsActivity.class)); - final ResolveInfo ri2 = mock(ResolveInfo.class); - ri2.nonLocalizedLabel = "label2"; - - mPackageManager.addResolveInfoForIntent(shortcut1, ri1); - mPackageManager.addResolveInfoForIntent(shortcut2, ri2); - + public void updatePinnedShortcuts_updatesAllShortcuts() { final List pinnedShortcuts = Arrays.asList( makeShortcut("d1"), makeShortcut("d2"), @@ -99,7 +82,7 @@ public class ShortcutsUpdateTaskTest { makeShortcut(Settings.SoundSettingsActivity.class)); when(mShortcutManager.getPinnedShortcuts()).thenReturn(pinnedShortcuts); - new ShortcutsUpdateTask(mContext).doInBackground(); + ShortcutsUpdater.updatePinnedShortcuts(mContext); verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); @@ -108,6 +91,52 @@ public class ShortcutsUpdateTaskTest { assertThat(updates).hasSize(2); assertThat(pinnedShortcuts.get(2).getId()).isEqualTo(updates.get(0).getId()); assertThat(pinnedShortcuts.get(4).getId()).isEqualTo(updates.get(1).getId()); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("App info"); + assertThat(updates.get(1).getShortLabel().toString()).isEqualTo("Sound & vibration"); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void updatePinnedShortcuts_withModesFlag_replacesDndByModes() { + List shortcuts = List.of( + makeShortcut(Settings.ZenModeSettingsActivity.class)); + when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts); + + ShortcutsUpdater.updatePinnedShortcuts(mContext); + + verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); + final List updates = mListCaptor.getValue(); + assertThat(updates).hasSize(1); + + // Id hasn't changed, but intent and label has. + ComponentName zenCn = new ComponentName(mContext, Settings.ZenModeSettingsActivity.class); + ComponentName modesCn = new ComponentName(mContext, Settings.ModesSettingsActivity.class); + assertThat(updates.get(0).getId()).isEqualTo( + SHORTCUT_ID_PREFIX + zenCn.flattenToShortString()); + assertThat(updates.get(0).getIntent().getComponent()).isEqualTo(modesCn); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("Modes"); + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + public void updatePinnedShortcuts_withoutModesFlag_leavesDndAlone() { + List shortcuts = List.of( + makeShortcut(Settings.ZenModeSettingsActivity.class)); + when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts); + + ShortcutsUpdater.updatePinnedShortcuts(mContext); + + verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); + final List updates = mListCaptor.getValue(); + assertThat(updates).hasSize(1); + + // Nothing has changed. + ComponentName zenCn = new ComponentName(mContext, Settings.ZenModeSettingsActivity.class); + assertThat(updates.get(0).getId()).isEqualTo( + SHORTCUT_ID_PREFIX + zenCn.flattenToShortString()); + assertThat(updates.get(0).getIntent().getComponent()).isEqualTo(zenCn); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("Do Not Disturb"); + } private ShortcutInfo makeShortcut(Class className) { diff --git a/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java b/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java index 3f6d785463d..230b443f296 100644 --- a/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java +++ b/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java @@ -118,7 +118,7 @@ public class WallpaperSuggestionActivityTest { } @Test - public void addExtras_intentNotFromSetupWizard_extrasHasFocusWallpaper() { + public void addExtras_intentNotFromSetupWizard_extrasHasFocusWallpaperAndLaunchedSettingsSearch() { WallpaperSuggestionActivity activity = Robolectric.buildActivity( WallpaperSuggestionActivity.class, new Intent(Intent.ACTION_MAIN).setComponent( new ComponentName(RuntimeEnvironment.application, @@ -127,6 +127,8 @@ public class WallpaperSuggestionActivityTest { assertThat(intent).isNotNull(); assertThat(intent.getStringExtra(WALLPAPER_FLAVOR)).isEqualTo("focus_wallpaper"); + assertThat(intent.getStringExtra(WALLPAPER_LAUNCH_SOURCE)) + .isEqualTo("app_launched_settings_search"); } diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt new file mode 100644 index 00000000000..69fa9c42e6c --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt @@ -0,0 +1,151 @@ +/* + * 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.network + +import android.content.Context +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.RestrictedPreference +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSummaryControllerTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val preference = RestrictedPreference(context).apply { key = KEY } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + private val mockMobileNetworkSummaryRepository = mock() + private val airplaneModeOnFlow = MutableStateFlow(false) + + private val controller = + MobileNetworkSummaryController( + context = context, + preferenceKey = KEY, + repository = mockMobileNetworkSummaryRepository, + airplaneModeOnFlow = airplaneModeOnFlow, + ) + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + @Test + fun onViewCreated_noSubscriptions(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.NoSubscriptions) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary).isNull() + assertThat(preference.isEnabled).isFalse() + assertThat(preference.onPreferenceClickListener).isNull() + } + + @Test + fun onViewCreated_addNetwork(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.AddNetwork) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary) + .isEqualTo(context.getString(R.string.mobile_network_summary_add_a_network)) + assertThat(preference.isEnabled).isTrue() + assertThat(preference.onPreferenceClickListener).isNotNull() + } + + @Test + fun onViewCreated_hasSubscriptions(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf( + MobileNetworkSummaryRepository.HasSubscriptions( + displayNames = listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary).isEqualTo("$DISPLAY_NAME_1, $DISPLAY_NAME_2") + assertThat(preference.isEnabled).isTrue() + assertThat(preference.fragment).isNotNull() + } + + @Test + fun onViewCreated_addNetworkAndAirplaneModeOn(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.AddNetwork) + } + airplaneModeOnFlow.value = true + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isFalse() + } + + @Test + fun onViewCreated_hasSubscriptionsAndAirplaneModeOn(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf( + MobileNetworkSummaryRepository.HasSubscriptions( + displayNames = listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + airplaneModeOnFlow.value = true + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isFalse() + } + + + private companion object { + const val KEY = "test_key" + const val DISPLAY_NAME_1 = "Display Name 1" + const val DISPLAY_NAME_2 = "Display Name 2" + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt new file mode 100644 index 00000000000..463af96268f --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt @@ -0,0 +1,101 @@ +/* + * 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.network + +import android.content.Context +import android.telephony.SubscriptionInfo +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSummaryRepositoryTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val mockSubscriptionRepository = mock() + private val mockEuiccRepository = mock() + + private val repository = + MobileNetworkSummaryRepository( + context = context, + subscriptionRepository = mockSubscriptionRepository, + euiccRepository = mockEuiccRepository, + getDisplayName = { it.displayName.toString() }, + ) + + @Test + fun subscriptionsStateFlow_noSubscriptionsAndShowEuicc_returnsAddNetwork() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn flowOf(emptyList()) + } + mockEuiccRepository.stub { on { showEuiccSettings() } doReturn true } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state).isEqualTo(MobileNetworkSummaryRepository.AddNetwork) + } + + @Test + fun subscriptionsStateFlow_noSubscriptionsAndHideEuicc_returnsNoSubscriptions() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn flowOf(emptyList()) + } + mockEuiccRepository.stub { on { showEuiccSettings() } doReturn false } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state).isEqualTo(MobileNetworkSummaryRepository.NoSubscriptions) + } + + @Test + fun subscriptionsStateFlow_hasSubscriptions_returnsHasSubscriptions() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn + flowOf( + listOf( + SubscriptionInfo.Builder().setDisplayName(DISPLAY_NAME_1).build(), + SubscriptionInfo.Builder().setDisplayName(DISPLAY_NAME_2).build(), + ) + ) + } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state) + .isEqualTo( + MobileNetworkSummaryRepository.HasSubscriptions( + listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + + private companion object { + const val DISPLAY_NAME_1 = "Sub 1" + const val DISPLAY_NAME_2 = "Sub 2" + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt index 90d0aa56d77..265cd33f3bf 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt @@ -27,7 +27,9 @@ import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.stub @@ -134,6 +136,15 @@ class VoNrRepositoryTest { assertThat(isVoNrEnabled).isTrue() } + @Test + fun isVoNrEnabledFlow_noPhoneProcess_noCrash() = runBlocking { + mockTelephonyManager.stub { on { isVoNrEnabled } doThrow IllegalStateException("no Phone") } + + val isVoNrEnabled = repository.isVoNrEnabledFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVoNrEnabled).isFalse() + } + @Test fun setVoNrEnabled(): Unit = runBlocking { repository.setVoNrEnabled(SUB_ID, true) @@ -141,7 +152,20 @@ class VoNrRepositoryTest { verify(mockTelephonyManager).setVoNrEnabled(true) } + @Test + fun setVoNrEnabled_noPhoneProcess_noCrash(): Unit = runBlocking { + mockTelephonyManager.stub { + on { + setVoNrEnabled(any()) + } doThrow IllegalStateException("no Phone") + } + + repository.setVoNrEnabled(SUB_ID, true) + + verify(mockTelephonyManager).setVoNrEnabled(true) + } + private companion object { const val SUB_ID = 1 } -} +} \ No newline at end of file