Snap for 12391343 from 6c584d7f42 to 24Q4-release

Change-Id: I7adbc4e226402d8fe76ca61f1ef6e6b84e9c5e66
This commit is contained in:
Android Build Coastguard Worker
2024-09-19 23:21:42 +00:00
41 changed files with 1676 additions and 875 deletions

View File

@@ -5362,6 +5362,14 @@
</intent-filter>
</receiver>
<receiver
android:name=".shortcut.ShortcutsUpdateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<!-- This is the longest AndroidManifest.xml ever. -->
</application>
</manifest>

View File

@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M487,600L392,505L346,551L376,580Q399,603 399,637Q399,671 376,694L333,737Q310,760 276.5,760Q243,760 220,737L103,620Q80,597 80,563.5Q80,530 103,507L146,464Q169,441 203,441Q237,441 260,464L289,494L335,448L160,273L273,160L448,335L493,290L464,260Q441,237 441,203Q441,169 464,146L507,103Q530,80 563.5,80Q597,80 620,103L737,220Q760,243 760,276.5Q760,310 737,333L694,376Q671,399 637,399Q603,399 580,376L550,347L505,392L600,487L487,600ZM520,880L520,800Q637,800 718.5,718.5Q800,637 800,520L880,520Q880,595 851.5,660.5Q823,726 774.5,774.5Q726,823 660.5,851.5Q595,880 520,880ZM520,720L520,640Q570,640 605,605Q640,570 640,520L720,520Q720,603 661.5,661.5Q603,720 520,720ZM520,203L550,233L593,190L563,160Q563,160 563,160Q563,160 563,160L520,203Q520,203 520,203Q520,203 520,203ZM160,563L190,593L233,550L203,520Q203,520 203,520Q203,520 203,520L160,563Q160,563 160,563Q160,563 160,563ZM637,320L680,277Q680,277 680,277Q680,277 680,277L650,247L607,290L637,320Q637,320 637,320Q637,320 637,320ZM277,680L320,637Q320,637 320,637Q320,637 320,637L290,607L247,650L277,680Q277,680 277,680Q277,680 277,680Z"/>

View File

@@ -8030,10 +8030,10 @@
<string name="spatial_audio_multi_toggle_off">Off</string>
<!-- Bluetooth device details: spatial audio is on. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_on">Off</string>
<string name="spatial_audio_multi_toggle_on">Fixed</string>
<!-- Bluetooth device details: head tracking is on. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_head_tracking_on">Off</string>
<string name="spatial_audio_multi_toggle_head_tracking_on">Head Tracking</string>
<!-- Zen Modes: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
<string name="zen_mode_settings_schedules_summary">
@@ -8249,8 +8249,7 @@
<!-- Do not disturb: device effect option, not first in list [CHAR LIMIT=80] -->
<string name="mode_dark_theme_title_secondary_list">enable dark theme</string>
<!-- Do not disturb: device effect summary [CHAR LIMIT=NONE] -->
<string name="mode_dark_theme_summary">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</string>
<string name="mode_dark_theme_summary">Switch the device theme to use a dark background, which is easier on the eyes</string>
<!-- [CHAR LIMIT=NONE] Zen mode settings: Summary for sound interruption settings -->
<string name="mode_display_settings_summary">
{count, plural, offset:2

View File

@@ -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" />
<com.android.settingslib.RestrictedSwitchPreference
android:key="airplane_mode"

View File

@@ -16,17 +16,19 @@
package com.android.settings.backup;
import android.app.backup.BackupAgentHelper;
import android.util.Log;
import com.android.settings.flags.Flags;
import com.android.settings.onboarding.OnboardingFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.shortcut.CreateShortcutPreferenceController;
import com.android.settings.shortcut.ShortcutsUpdater;
import com.android.settingslib.datastore.BackupRestoreStorageManager;
/** Backup agent for Settings APK */
public class SettingsBackupHelper extends BackupAgentHelper {
private static final String TAG = "SettingsBackupHelper";
public static final String SOUND_BACKUP_HELPER = "SoundSettingsBackup";
public static final String ACCESSIBILITY_APPEARANCE_BACKUP_HELPER =
"AccessibilityAppearanceSettingsBackup";
@@ -58,6 +60,10 @@ public class SettingsBackupHelper extends BackupAgentHelper {
public void onRestoreFinished() {
super.onRestoreFinished();
BackupRestoreStorageManager.getInstance(this).onRestoreFinished();
CreateShortcutPreferenceController.updateRestoredShortcuts(this);
try {
ShortcutsUpdater.updatePinnedShortcuts(this);
} catch (Exception e) {
Log.e(TAG, "Error updating shortcuts after restoring backup", e);
}
}
}

View File

@@ -93,6 +93,7 @@ public class UdfpsEnrollEnrollingView extends GlifLayout {
} else if (mShouldUseReverseLandscape) {
swapHeaderAndContent();
}
mUdfpsEnrollView.setVisibility(View.VISIBLE);
setOnHoverListener();
}

View File

@@ -18,32 +18,94 @@ package com.android.settings.bluetooth;
import static android.os.UserManager.DISALLOW_CONFIG_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 android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.accessibility.AccessibilityStatsLogUtils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingIncompatibleDialogFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Abstract class for providing basic interaction for a list of Bluetooth devices in bluetooth
* device pairing detail page.
*/
public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment {
private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10);
private static final int AUTO_DISMISS_MESSAGE_ID = 1001;
protected boolean mInitialScanStarted;
@VisibleForTesting
protected BluetoothProgressCategory mAvailableDevicesCategory;
@Nullable
private volatile BluetoothDevice mJustBonded = null;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@Nullable
private AlertDialog mLoadingDialog = null;
@VisibleForTesting
boolean mShouldTriggerAudioSharingShareThenPairFlow = false;
private CopyOnWriteArrayList<BluetoothDevice> 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);
}
}

View File

@@ -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<Int?>(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))
}
}

View File

@@ -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() {

View File

@@ -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<Integer, Object>[] 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();
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<Integer, Object>[] 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<BluetoothDevice> 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;
}

View File

@@ -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<LocaleStore.LocaleInfo> 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;

View File

@@ -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<SubscriptionInfoEntity> mSubInfoEntityList;
private List<UiccInfoEntity> mUiccInfoEntityList;
private List<MobileNetworkInfoEntity> 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:
* <ul>
* <li>No subscription: click action begins a UI flow to add a network subscription, and
* the summary text indicates this</li>
*
* <li>One subscription: click action takes you to details for that one network, and
* the summary text is the network name</li>
*
* <li>More than one subscription: click action takes you to a page listing the subscriptions,
* and the summary text gives the count of SIMs</li>
* </ul>
*/
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<SubscriptionInfoEntity> subInfoEntityList) {
mSubInfoEntityList = subInfoEntityList;
update();
}
@Override
public void onAllUiccInfoChanged(List<UiccInfoEntity> uiccInfoEntityList) {
mUiccInfoEntityList = uiccInfoEntityList;
update();
}
@Override
public void onAllMobileNetworkInfoChanged(
List<MobileNetworkInfoEntity> mobileNetworkInfoEntityList) {
mMobileNetworkInfoEntityList = mobileNetworkInfoEntityList;
update();
}
}

View File

@@ -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<Boolean> =
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),
)
}
}

View File

@@ -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<String>) : SubscriptionsState
fun subscriptionsStateFlow(): Flow<SubscriptionsState> =
subDisplayNamesFlow()
.map { displayNames ->
if (displayNames.isEmpty()) {
if (euiccRepository.showEuiccSettings()) AddNetwork else NoSubscriptions
} else {
HasSubscriptions(displayNames)
}
}
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
private fun subDisplayNamesFlow(): Flow<List<String>> =
subscriptionRepository
.selectableSubscriptionInfoListFlow()
.map { subInfos -> subInfos.map(getDisplayName) }
.distinctUntilChanged()
.conflate()
.flowOn(Dispatchers.Default)
}

View File

@@ -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<AbstractPreferenceController> createPreferenceControllers(Context context) {
return buildPreferenceControllers(context, getSettingsLifecycle(),
this /* LifecycleOwner */);
return buildPreferenceControllers(context, getSettingsLifecycle());
}
private static List<AbstractPreferenceController> 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<AbstractPreferenceController> 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<AbstractPreferenceController> createPreferenceControllers(Context
context) {
return buildPreferenceControllers(context, null /* lifecycle */,
null /* LifecycleOwner */);
return buildPreferenceControllers(context, null /* lifecycle */);
}
};
}

View File

@@ -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"
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<ShortcutInfo> 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<ResolveInfo> SHORTCUT_COMPARATOR =
(i1, i2) -> i1.priority - i2.priority;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
});
}
}
}

View File

@@ -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<Void, Void, Void> {
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<ShortcutInfo> 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;
}
}

View File

@@ -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.
*
* <p>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<ShortcutInfo> 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;
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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<Drawable, String> 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<Intent> 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 {

View File

@@ -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();
}

View File

@@ -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<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
private static final Pair<Integer, Object>[] 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);

View File

@@ -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();

View File

@@ -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<Fragment> 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);

View File

@@ -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<Intent> 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<Intent> 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<Boolean> 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<Boolean> 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<Boolean> 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));
}
}

View File

@@ -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();
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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<ShortcutInfo> 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<ShortcutInfo> shortcuts = List.of(
makeShortcut(Settings.ZenModeSettingsActivity.class));
when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts);
ShortcutsUpdater.updatePinnedShortcuts(mContext);
verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture());
final List<ShortcutInfo> 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<ShortcutInfo> shortcuts = List.of(
makeShortcut(Settings.ZenModeSettingsActivity.class));
when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts);
ShortcutsUpdater.updatePinnedShortcuts(mContext);
verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture());
final List<ShortcutInfo> 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) {

View File

@@ -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");
}

View File

@@ -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<MobileNetworkSummaryRepository>()
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"
}
}

View File

@@ -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<SubscriptionRepository>()
private val mockEuiccRepository = mock<EuiccRepository>()
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"
}
}

View File

@@ -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
}
}
}