Snap for 11343021 from c36baa0e8d to 24Q2-release

Change-Id: I624b9ae0bdad5c0b76df84c765def6239c3bffa8
This commit is contained in:
Android Build Coastguard Worker
2024-01-23 00:21:14 +00:00
36 changed files with 1933 additions and 211 deletions

View File

@@ -4603,6 +4603,22 @@
android:value="true" />
</activity>
<activity
android:name="Settings$AudioStreamConfirmDialogActivity"
android:exported="true"
android:theme="@style/Transparent"
android:permission="android.permission.BLUETOOTH_CONNECT"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter android:priority="1">
<action android:name="android.settings.AUDIO_STREAM_DIALOG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialog" />
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
<activity
android:name=".Settings$PreviouslyConnectedDeviceActivity"
android:label="@string/connected_device_saved_title"

View File

@@ -6,3 +6,10 @@ flag {
description: "Gates whether to offload bluetooth operations to background thread"
bug: "305636727"
}
flag {
name: "enable_bluetooth_profile_toggle_visibility_checker"
namespace: "pixel_cross_device_control"
description: "Gates whether to enable checker for bluetooth profile toggle visibility"
bug: "321178209"
}

View File

@@ -103,9 +103,11 @@
<Switch
android:id="@+id/phonebook_sharing_message_confirm_pin"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="48dp"
android:layout_weight="0"
android:gravity="center_vertical" />
android:gravity="center_vertical"
android:contentDescription="@string/bluetooth_pairing_phonebook_toggle_text"
android:switchMinWidth="48dp" />
</LinearLayout>
</LinearLayout>

View File

@@ -11538,7 +11538,7 @@
<!-- Button on the dual sim onboarding to go to next page. [CHAR LIMIT=30] -->
<string name="sim_onboarding_next">Next</string>
<!-- Text on the progressbar of dual sim onboarding for turning sim on. [CHAR LIMIT=30] -->
<string name="sim_onboarding_profressbar_turning_sim_on">Turning on <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g>&#8230;</string>
<string name="sim_onboarding_progressbar_turning_sim_on">Turning on <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g>&#8230;</string>
<!-- Title of service provider name(SPN) at mobile network settings page. [CHAR LIMIT=30] -->
<string name="mobile_network_spn_title">Mobile network</string>
<!-- Title of phone number at mobile network settings page. [CHAR LIMIT=30] -->

View File

@@ -78,14 +78,14 @@
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_weight="1"
android:visibility="gone"/>
android:visibility="invisible"/>
<Button
android:id="@+id/right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginRight="16dp"
android:visibility="gone"/>
android:visibility="invisible"/>
</LinearLayout>
</LinearLayout>

View File

@@ -399,6 +399,7 @@ public class Settings extends SettingsActivity {
public static class StylusUsiDetailsActivity extends SettingsActivity { /* empty */ }
public static class BluetoothBroadcastActivity extends SettingsActivity { /* empty */ }
public static class BluetoothFindBroadcastsActivity extends SettingsActivity { /* empty */ }
public static class AudioStreamConfirmDialogActivity extends SettingsActivity { /* empty */ }
public static class WifiCallingDisclaimerActivity extends SettingsActivity { /* empty */ }
public static class MobileNetworkListActivity extends SettingsActivity {}
public static class PowerMenuSettingsActivity extends SettingsActivity {}

View File

@@ -37,6 +37,8 @@ import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -49,11 +51,14 @@ import com.android.settingslib.bluetooth.MapProfile;
import com.android.settingslib.bluetooth.PanProfile;
import com.android.settingslib.bluetooth.PbapServerProfile;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
/**
* This class adds switches for toggling the individual profiles that a Bluetooth device
@@ -79,6 +84,8 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
"persist.bluetooth.leaudio.toggle_visible";
private final AtomicReference<Set<String>> mInvisiblePreferenceKey = new AtomicReference<>();
private LocalBluetoothManager mManager;
private LocalBluetoothProfileManager mProfileManager;
private CachedBluetoothDevice mCachedDevice;
@@ -547,6 +554,22 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
*/
@Override
protected void refresh() {
if (Flags.enableBluetoothProfileToggleVisibilityChecker()) {
ThreadUtils.postOnBackgroundThread(
() -> {
mInvisiblePreferenceKey.set(
FeatureFactory.getFeatureFactory()
.getBluetoothFeatureProvider()
.getInvisibleProfilePreferenceKeys(
mContext, mCachedDevice.getDevice()));
ThreadUtils.postOnMainThread(this::refreshUi);
});
} else {
refreshUi();
}
}
private void refreshUi() {
for (LocalBluetoothProfile profile : getProfiles()) {
if (profile == null || !profile.isProfileReady()) {
continue;
@@ -577,6 +600,16 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
preference.setSelectable(false);
mProfilesContainer.addPreference(preference);
}
if (Flags.enableBluetoothProfileToggleVisibilityChecker()) {
Set<String> invisibleKeys = mInvisiblePreferenceKey.get();
if (invisibleKeys != null) {
for (int i = 0; i < mProfilesContainer.getPreferenceCount(); ++i) {
Preference pref = mProfilesContainer.getPreference(i);
pref.setVisible(pref.isVisible() && !invisibleKeys.contains(pref.getKey()));
}
}
}
}
@Override

View File

@@ -27,6 +27,7 @@ import androidx.preference.Preference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import java.util.List;
import java.util.Set;
/**
* Provider for bluetooth related features.
@@ -73,4 +74,14 @@ public interface BluetoothFeatureProvider {
* @return the extra bluetooth preference list
*/
List<Preference> getBluetoothExtraOptions(Context context, CachedBluetoothDevice device);
/**
* Gets the bluetooth profile preference keys which should be hidden in the device details page.
*
* @param context Context
* @param bluetoothDevice the bluetooth device
* @return the profiles which should be hidden
*/
Set<String> getInvisibleProfilePreferenceKeys(
Context context, BluetoothDevice bluetoothDevice);
}

View File

@@ -29,8 +29,10 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Set;
/**
* Impl of {@link BluetoothFeatureProvider}
@@ -65,4 +67,10 @@ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
CachedBluetoothDevice device) {
return ImmutableList.of();
}
@Override
public Set<String> getInvisibleProfilePreferenceKeys(
Context context, BluetoothDevice bluetoothDevice) {
return ImmutableSet.of();
}
}

View File

@@ -139,16 +139,20 @@ public class AudioSharingCompatibilityPreferenceController extends TogglePrefere
@Override
public boolean isChecked() {
// TODO: return real compatibility config.
return false;
return mBroadcast != null && mBroadcast.getImproveCompatibility();
}
@Override
public boolean setChecked(boolean isChecked) {
if (mBroadcast == null) {
if (mBroadcast == null || mBroadcast.getImproveCompatibility() == isChecked) {
Log.d(
TAG,
"Skip setting improveCompatibility, unchanged = "
+ (mBroadcast.getImproveCompatibility() == isChecked));
return false;
}
// TODO: set real compatibility config.
mBroadcast.setImproveCompatibility(isChecked);
// TODO: call updateBroadcast once framework change ready.
return true;
}

View File

@@ -22,7 +22,6 @@ import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroupSettings;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
@@ -419,7 +418,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
if (isBroadcasting()) {
// Show stop audio sharing dialog when an ineligible (non LE audio) remote device
// connected during a sharing session.
ThreadUtils.postOnMainThread(
postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingStopDialogFragment.show(
@@ -443,8 +442,9 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
if (isBroadcasting()) {
if (groupedDevices.containsKey(cachedDevice.getGroupId())
&& groupedDevices.get(cachedDevice.getGroupId()).stream()
int groupId = AudioSharingUtils.getGroupId(cachedDevice);
if (groupedDevices.containsKey(groupId)
&& groupedDevices.get(groupId).stream()
.anyMatch(
device ->
AudioSharingUtils.hasBroadcastSource(
@@ -464,7 +464,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
// Show audio sharing switch dialog when the third eligible (LE audio) remote device
// connected during a sharing session.
if (deviceItemsInSharingSession.size() >= 2) {
ThreadUtils.postOnMainThread(
postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingDisconnectDialogFragment.show(
@@ -495,7 +495,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
} else {
// Show audio sharing join dialog when the first or second eligible (LE audio)
// remote device connected during a sharing session.
ThreadUtils.postOnMainThread(
postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingJoinDialogFragment.show(
@@ -516,7 +516,8 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
// Use random device in the group within the sharing session to represent the group.
CachedBluetoothDevice device = devices.get(0);
if (device.getGroupId() == cachedDevice.getGroupId()) {
if (AudioSharingUtils.getGroupId(device)
== AudioSharingUtils.getGroupId(cachedDevice)) {
continue;
}
deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
@@ -524,7 +525,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
// Show audio sharing join dialog when the second eligible (LE audio) remote
// device connect and no sharing session.
if (deviceItems.size() == 1) {
ThreadUtils.postOnMainThread(
postOnMainThread(
() -> {
closeOpeningDialogs();
AudioSharingJoinDialogFragment.show(
@@ -539,8 +540,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
mTargetSinks.add(device.getDevice());
}
}
mBroadcast.startPrivateBroadcast(
BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH);
mBroadcast.startPrivateBroadcast();
});
});
}
@@ -601,4 +601,8 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro
}
}
}
private void postOnMainThread(@NonNull Runnable runnable) {
mContext.getMainExecutor().execute(runnable);
}
}

View File

@@ -77,11 +77,12 @@ public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePre
CachedBluetoothDevice cachedDevice =
mLocalBtManager.getCachedDeviceManager().findDevice(device);
if (cachedDevice == null) return;
mValueMap.put(cachedDevice.getGroupId(), volume);
int groupId = AudioSharingUtils.getGroupId(cachedDevice);
mValueMap.put(groupId, volume);
for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
if (preference.getCachedDevice() != null
&& preference.getCachedDevice().getGroupId()
== cachedDevice.getGroupId()) {
&& AudioSharingUtils.getGroupId(preference.getCachedDevice())
== groupId) {
// If the callback return invalid volume, try to
// get the volume from AudioManager.STREAM_MUSIC
int finalVolume = getAudioVolumeIfNeeded(volume);
@@ -270,7 +271,7 @@ public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePre
if (volumePref.getProgress() > 0) return;
CachedBluetoothDevice device = volumePref.getCachedDevice();
if (device == null) return;
int volume = mValueMap.getOrDefault(device.getGroupId(), -1);
int volume = mValueMap.getOrDefault(AudioSharingUtils.getGroupId(device), -1);
// If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC
int finalVolume = getAudioVolumeIfNeeded(volume);
Log.d(

View File

@@ -22,7 +22,6 @@ import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroupSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -339,7 +338,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
}
mDeviceItemsForSharing.remove(0);
}
mBroadcast.startPrivateBroadcast(BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH);
mBroadcast.startPrivateBroadcast();
}
private void stopAudioSharing() {

View File

@@ -29,10 +29,11 @@ import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import java.util.ArrayList;
import java.util.HashMap;
@@ -57,10 +58,16 @@ public class AudioSharingUtils {
public static Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId(
LocalBluetoothManager localBtManager) {
Map<Integer, List<CachedBluetoothDevice>> groupedDevices = new HashMap<>();
if (localBtManager == null) {
Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to bt manager is null");
return groupedDevices;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) return groupedDevices;
// TODO: filter out devices with le audio disabled.
if (assistant == null) {
Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to assistant profile is null");
return groupedDevices;
}
List<BluetoothDevice> connectedDevices = assistant.getConnectedDevices();
CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
for (BluetoothDevice device : connectedDevices) {
@@ -69,7 +76,7 @@ public class AudioSharingUtils {
Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
continue;
}
int groupId = cachedDevice.getGroupId();
int groupId = getGroupId(cachedDevice);
if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
Log.d(
TAG,
@@ -105,9 +112,6 @@ public class AudioSharingUtils {
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
boolean filterByInSharing) {
List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) return orderedDevices;
for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
CachedBluetoothDevice leadDevice = null;
for (CachedBluetoothDevice device : devices) {
@@ -191,7 +195,7 @@ public class AudioSharingUtils {
CachedBluetoothDevice cachedDevice) {
return new AudioSharingDeviceItem(
cachedDevice.getName(),
cachedDevice.getGroupId(),
getGroupId(cachedDevice),
isActiveLeAudioDevice(cachedDevice));
}
@@ -204,19 +208,36 @@ public class AudioSharingUtils {
*/
public static boolean hasBroadcastSource(
CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
if (localBtManager == null) {
Log.d(TAG, "Skip check hasBroadcastSource due to bt manager is null");
return false;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
Log.d(TAG, "Skip check hasBroadcastSource due to assistant profile is null");
return false;
}
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()) return true;
if (!sourceList.isEmpty()) {
Log.d(
TAG,
"Lead device has broadcast source, device = "
+ cachedDevice.getDevice().getAnonymizedAddress());
return true;
}
// Return true if member device is in broadcast.
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty()) return true;
if (!list.isEmpty()) {
Log.d(
TAG,
"Member device has broadcast source, device = "
+ device.getDevice().getAnonymizedAddress());
return true;
}
}
return false;
}
@@ -257,8 +278,8 @@ public class AudioSharingUtils {
/** Toast message on main thread. */
public static void toastMessage(Context context, String message) {
ThreadUtils.postOnMainThread(
() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
context.getMainExecutor()
.execute(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
}
/** Returns if the le audio sharing is enabled. */
@@ -273,7 +294,10 @@ public class AudioSharingUtils {
/** Automatically update active device if needed. */
public static void updateActiveDeviceIfNeeded(LocalBluetoothManager localBtManager) {
if (localBtManager == null) return;
if (localBtManager == null) {
Log.d(TAG, "Skip updateActiveDeviceIfNeeded due to bt manager is null");
return;
}
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices =
fetchConnectedDevicesByGroupId(localBtManager);
List<CachedBluetoothDevice> devicesInSharing =
@@ -283,6 +307,7 @@ public class AudioSharingUtils {
List<BluetoothDevice> devices =
BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
CachedBluetoothDevice targetDevice = null;
// Find the earliest connected device in sharing session.
int targetDeviceIdx = -1;
for (CachedBluetoothDevice device : devicesInSharing) {
if (devices.contains(device.getDevice())) {
@@ -299,6 +324,14 @@ public class AudioSharingUtils {
"updateActiveDeviceIfNeeded, set active device: "
+ targetDevice.getDevice().getAnonymizedAddress());
targetDevice.setActive();
} else {
Log.d(
TAG,
"updateActiveDeviceIfNeeded, skip set active device: "
+ (targetDevice == null
? "null"
: (targetDevice.getDevice().getAnonymizedAddress()
+ " is already active")));
}
}
@@ -312,9 +345,38 @@ public class AudioSharingUtils {
/** Stops the latest broadcast. */
public static void stopBroadcasting(LocalBluetoothManager manager) {
if (manager == null) return;
if (manager == null) {
Log.d(TAG, "Skip stop broadcasting due to bt manager is null");
return;
}
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
if (broadcast == null) {
Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
}
broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
}
/**
* Get CSIP group id for {@link CachedBluetoothDevice}.
*
* <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
* LeAudioProfile#getGroupId.
*/
public static int getGroupId(CachedBluetoothDevice cachedDevice) {
int groupId = cachedDevice.getGroupId();
String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
return groupId;
}
for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
if (profile instanceof LeAudioProfile) {
Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
}
}
Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.connecteddevice.audiosharing.audiostreams;
import android.app.Activity;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.os.Bundle;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.google.common.base.Strings;
public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
private static final String TAG = "AudioStreamConfirmDialog";
private Activity mActivity;
private String mBroadcastMetadataStr;
private BluetoothLeBroadcastMetadata mBroadcastMetadata;
private boolean mIsRequestValid = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setShowsDialog(true);
mActivity = getActivity();
if (mActivity == null) {
Log.w(TAG, "onCreate() mActivity is null!");
return;
}
mBroadcastMetadataStr =
mActivity.getIntent().getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
if (Strings.isNullOrEmpty(mBroadcastMetadataStr)) {
Log.w(TAG, "onCreate() mBroadcastMetadataStr is null or empty!");
return;
}
mBroadcastMetadata =
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
mBroadcastMetadataStr);
if (mBroadcastMetadata == null) {
Log.w(TAG, "onCreate() mBroadcastMetadata is null!");
} else {
// Warm up LE_AUDIO_BROADCAST_ASSISTANT service
Utils.getLocalBluetoothManager(mActivity);
mIsRequestValid = true;
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return mIsRequestValid ? getConfirmDialog() : getErrorDialog();
}
@Override
public int getMetricsCategory() {
// TODO(chelseahao): update metrics id
return 0;
}
private Dialog getConfirmDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Listen to audio stream")
.setSubTitle1(mBroadcastMetadata.getBroadcastName())
.setSubTitle2(
"The audio stream will play on the active LE audio device. Use this device"
+ " to control the volume.")
.setLeftButtonText("Cancel")
.setLeftButtonOnClickListener(
unused -> {
dismiss();
mActivity.finish();
})
.setRightButtonText("Listen")
.setRightButtonOnClickListener(
unused -> {
launchAudioStreamsActivity();
dismiss();
mActivity.finish();
})
.build();
}
private Dialog getErrorDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Can't listen to audio stream")
.setSubTitle1("Can't play this audio stream. Learn more")
.setRightButtonText("Close")
.setRightButtonOnClickListener(
unused -> {
dismiss();
mActivity.finish();
})
.build();
}
private void launchAudioStreamsActivity() {
Bundle bundle = new Bundle();
bundle.putString(KEY_BROADCAST_METADATA, mBroadcastMetadataStr);
new SubSettingLauncher(mActivity)
.setTitleRes(R.string.bluetooth_find_broadcast_title)
.setDestination(AudioStreamsDashboardFragment.class.getName())
.setArguments(bundle)
.setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
.launch();
}
}

View File

@@ -31,6 +31,8 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.google.common.base.Strings;
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D;
@@ -72,6 +74,21 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
use(AudioStreamsScanQrCodeController.class).setFragment(this);
mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
mAudioStreamsProgressCategoryController.setFragment(this);
if (getArguments() != null) {
String broadcastMetadataStr =
getArguments().getString(AudioStreamConfirmDialog.KEY_BROADCAST_METADATA);
if (!Strings.isNullOrEmpty(broadcastMetadataStr)) {
BluetoothLeBroadcastMetadata broadcastMetadata =
BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
broadcastMetadataStr);
if (broadcastMetadata == null) {
Log.w(TAG, "onAttach() broadcastMetadata is null!");
} else {
mAudioStreamsProgressCategoryController.setSourceFromQrCode(broadcastMetadata);
}
}
}
}
@Override

View File

@@ -84,6 +84,7 @@ import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFrag
import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
import com.android.settings.connecteddevice.NfcAndPaymentFragment;
import com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialog;
import com.android.settings.connecteddevice.stylus.StylusUsiDetailsFragment;
import com.android.settings.connecteddevice.usb.UsbDetailsFragment;
import com.android.settings.datausage.DataSaverSummary;
@@ -349,6 +350,7 @@ public class SettingsGateway {
DataUsageList.class.getName(),
ToggleBackupSettingFragment.class.getName(),
PreviouslyConnectedDeviceDashboardFragment.class.getName(),
AudioStreamConfirmDialog.class.getName(),
BatterySaverScheduleSettings.class.getName(),
MobileNetworkListFragment.class.getName(),
PowerMenuSettings.class.getName(),

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2023 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 android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.telephony.UiccCardInfo
import android.telephony.UiccSlotInfo
import android.util.Log
import com.android.settingslib.utils.ThreadUtils
private const val TAG = "SimOnboardingService"
private const val INVALID = -1
class SimOnboardingService {
var subscriptionManager:SubscriptionManager? = null
var telephonyManager:TelephonyManager? = null
var targetSubId: Int = INVALID
var targetSubInfo: SubscriptionInfo? = null
var availableSubInfoList: List<SubscriptionInfo> = listOf()
var activeSubInfoList: List<SubscriptionInfo> = listOf()
var slotInfoList: List<UiccSlotInfo> = listOf()
var uiccCardInfoList: List<UiccCardInfo> = listOf()
var selectedSubInfoList: MutableList<SubscriptionInfo> = mutableListOf()
var targetPrimarySimCalls: Int = -1
var targetPrimarySimTexts: Int = -1
var targetPrimarySimMobileData: Int = -1
var isMultipleEnabledProfilesSupported: Boolean = false
get() {
if (uiccCardInfoList.isEmpty()) {
Log.w(TAG, "UICC cards info list is empty.")
return false
}
return uiccCardInfoList.stream()
.anyMatch { cardInfo: UiccCardInfo -> cardInfo.isMultipleEnabledProfilesSupported }
}
var renameMutableMap : MutableMap<Int, String> = mutableMapOf()
fun isValid(): Boolean {
return targetSubId != INVALID
&& targetSubInfo != null
&& activeSubInfoList.isNotEmpty()
&& slotInfoList.isNotEmpty()
&& selectedSubInfoList.isNotEmpty()
}
fun clear() {
targetSubId = -1
targetSubInfo = null
availableSubInfoList = listOf()
activeSubInfoList = listOf()
slotInfoList = listOf()
uiccCardInfoList = listOf()
selectedSubInfoList = mutableListOf()
targetPrimarySimCalls = -1
targetPrimarySimTexts = -1
targetPrimarySimMobileData = -1
renameMutableMap.clear()
}
fun initData(inputTargetSubId:Int,context: Context) {
targetSubId = inputTargetSubId
subscriptionManager = context.getSystemService(SubscriptionManager::class.java)
telephonyManager = context.getSystemService(TelephonyManager::class.java)
ThreadUtils.postOnBackgroundThread {
activeSubInfoList = SubscriptionUtil.getActiveSubscriptions(subscriptionManager)
availableSubInfoList = SubscriptionUtil.getAvailableSubscriptions(context)
targetSubInfo = availableSubInfoList.find { subInfo -> subInfo.subscriptionId == targetSubId }
Log.d(
TAG, "targetSubId: $targetSubId" + ", targetSubInfo: $targetSubInfo" +
". activeSubInfoList: $activeSubInfoList"
)
slotInfoList = telephonyManager?.uiccSlotsInfo?.toList() ?: listOf()
Log.d(TAG, "slotInfoList: $slotInfoList.")
uiccCardInfoList = telephonyManager?.uiccCardsInfo!!
Log.d(TAG, "uiccCardInfoList: $uiccCardInfoList")
Log.d(TAG, "isMultipleEnabledProfilesSupported: $isMultipleEnabledProfilesSupported")
}
}
fun getSelectableSubscriptionInfo(): List<SubscriptionInfo> {
var list: MutableList<SubscriptionInfo> = mutableListOf()
list.addAll(activeSubInfoList)
if (!list.contains(targetSubInfo)) {
targetSubInfo?.let { list.add(it) }
}
Log.d(TAG, "list: $list")
return list.toList()
}
fun addItemForRenaming(subInfo: SubscriptionInfo, newName: String) {
if (subInfo.displayName == newName) {
return
}
renameMutableMap[subInfo.subscriptionId] = newName
}
fun getSubscriptionInfoDisplayName(subInfo: SubscriptionInfo): String {
return renameMutableMap[subInfo.subscriptionId] ?: subInfo.displayName.toString()
}
fun startActivatingSim(callback:() -> Unit){
// TODO: start to activate sim
}
}

View File

@@ -42,12 +42,14 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.telephony.MccTable;
import com.android.internal.telephony.flags.Flags;
import com.android.settings.R;
import com.android.settings.flags.Flags;
import com.android.settings.network.helper.SelectableSubscriptions;
import com.android.settings.network.helper.SubscriptionAnnotation;
import com.android.settings.network.telephony.DeleteEuiccSubscriptionDialogActivity;
import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity;
import com.android.settings.spa.SpaActivity;
import com.android.settings.spa.network.SimOnboardingPageProvider;
import java.util.ArrayList;
import java.util.Collections;
@@ -335,8 +337,16 @@ public class SubscriptionUtil {
if (duplicateOriginalNames.contains(info.originalName)) {
// This may return null, if the user cannot view the phone number itself.
final String phoneNumber = getBidiFormattedPhoneNumber(context,
info.subscriptionInfo);
String phoneNumber = "";
try {
final SubscriptionManager subscriptionManager = context.getSystemService(
SubscriptionManager.class);
phoneNumber = subscriptionManager.getPhoneNumber(infoSubId);
} catch (IllegalStateException
| SecurityException
| UnsupportedOperationException e) {
Log.w(TAG, "get number error." + e);
}
String lastFourDigits = "";
if (phoneNumber != null) {
lastFourDigits = (phoneNumber.length() > 4)
@@ -535,6 +545,11 @@ public class SubscriptionUtil {
Log.i(TAG, "Unable to toggle subscription due to invalid subscription ID.");
return;
}
if (enable && Flags.isDualSimOnboardingEnabled()) {
String route = SimOnboardingPageProvider.INSTANCE.getRoute(subId);
SpaActivity.startSpaActivity(context, route);
return;
}
context.startActivity(ToggleSubscriptionDialogActivity.getIntent(context, subId, enable));
}
@@ -814,7 +829,7 @@ public class SubscriptionUtil {
private static boolean isEmbeddedSubscriptionVisible(@NonNull SubscriptionInfo subInfo) {
if (subInfo.isEmbedded()
&& (subInfo.getProfileClass() == PROFILE_CLASS_PROVISIONING
|| (Flags.oemEnabledSatelliteFlag()
|| (com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag()
&& subInfo.isOnlyNonTerrestrialNetwork()))) {
return false;
}

View File

@@ -40,6 +40,10 @@ import androidx.navigation.navArgument
import com.android.settings.R
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeDisplayNames
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState
import com.android.settings.network.apn.ApnTypes.APN_TYPES_OPTIONS
import com.android.settings.network.apn.ApnTypes.APN_TYPE_MMS
import com.android.settings.network.apn.ApnTypes.getApnTypeSelectedOptionsState
import com.android.settings.network.apn.ApnTypes.updateApnType
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.LocalNavController
import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -100,6 +104,9 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState<ApnData>, uriInit: Ur
val networkTypeSelectedOptionsState = remember {
getNetworkTypeSelectedOptionsState(apnData.networkType)
}
var apnTypeSelectedOptionsState = remember {
getApnTypeSelectedOptionsState(apnData.apnType)
}
val navController = LocalNavController.current
var valid: String?
RegularScaffold(
@@ -191,37 +198,50 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState<ApnData>, uriInit: Ur
label = stringResource(R.string.apn_server),
enabled = apnData.serverEnabled
) { apnData = apnData.copy(server = it) }
SettingsOutlinedTextField(
value = apnData.mmsc,
label = stringResource(R.string.apn_mmsc),
errorMessage = validateMMSC(apnData.validEnabled, apnData.mmsc, context),
enabled = apnData.mmscEnabled
) { apnData = apnData.copy(mmsc = it) }
SettingsOutlinedTextField(
value = apnData.mmsProxy,
label = stringResource(R.string.apn_mms_proxy),
enabled = apnData.mmsProxyEnabled
) { apnData = apnData.copy(mmsProxy = it) }
SettingsOutlinedTextField(
value = apnData.mmsPort,
label = stringResource(R.string.apn_mms_port),
enabled = apnData.mmsPortEnabled
) { apnData = apnData.copy(mmsPort = it) }
SettingsExposedDropdownMenuCheckBox(
label = stringResource(R.string.apn_type),
options = APN_TYPES_OPTIONS,
selectedOptionsState = apnTypeSelectedOptionsState,
enabled = apnData.apnTypeEnabled,
errorMessage = validateAPNType(
apnData.validEnabled, apnData.apnType,
apnData.customizedConfig.readOnlyApnTypes, context
)
) {
val apnType = updateApnType(
apnTypeSelectedOptionsState,
apnData.customizedConfig.defaultApnTypes,
apnData.customizedConfig.readOnlyApnTypes
)
apnTypeSelectedOptionsState = getApnTypeSelectedOptionsState(apnType)
apnData = apnData.copy(
apnType = apnType
)
}
if (apnTypeSelectedOptionsState.contains(APN_TYPES_OPTIONS.indexOf(APN_TYPE_MMS))) {
SettingsOutlinedTextField(
value = apnData.mmsc,
label = stringResource(R.string.apn_mmsc),
errorMessage = validateMMSC(apnData.validEnabled, apnData.mmsc, context),
enabled = apnData.mmscEnabled
) { apnData = apnData.copy(mmsc = it) }
SettingsOutlinedTextField(
value = apnData.mmsProxy,
label = stringResource(R.string.apn_mms_proxy),
enabled = apnData.mmsProxyEnabled
) { apnData = apnData.copy(mmsProxy = it) }
SettingsOutlinedTextField(
value = apnData.mmsPort,
label = stringResource(R.string.apn_mms_port),
enabled = apnData.mmsPortEnabled
) { apnData = apnData.copy(mmsPort = it) }
}
SettingsExposedDropdownMenuBox(
label = stringResource(R.string.apn_auth_type),
options = authTypeOptions,
selectedOptionIndex = apnData.authType,
enabled = apnData.authTypeEnabled,
) { apnData = apnData.copy(authType = it) }
SettingsOutlinedTextField(
value = apnData.apnType,
label = stringResource(R.string.apn_type),
enabled = apnData.apnTypeEnabled,
errorMessage = validateAPNType(
apnData.validEnabled, apnData.apnType,
apnData.customizedConfig.readOnlyApnTypes, context
)
) { apnData = apnData.copy(apnType = updateApnType(apnData.copy(apnType = it))) }
SettingsExposedDropdownMenuBox(
label = stringResource(R.string.apn_protocol),
options = apnProtocolOptions,
@@ -234,6 +254,13 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState<ApnData>, uriInit: Ur
selectedOptionIndex = apnData.apnRoaming,
enabled = apnData.apnRoamingEnabled
) { apnData = apnData.copy(apnRoaming = it) }
SettingsExposedDropdownMenuCheckBox(
label = stringResource(R.string.network_type),
options = getNetworkTypeDisplayNames(),
selectedOptionsState = networkTypeSelectedOptionsState,
emptyVal = stringResource(R.string.network_type_unspecified),
enabled = apnData.networkTypeEnabled
) {}
SwitchPreference(
object : SwitchPreferenceModel {
override val title = context.resources.getString(R.string.carrier_enabled)
@@ -244,13 +271,6 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState<ApnData>, uriInit: Ur
}
}
)
SettingsExposedDropdownMenuCheckBox(
label = stringResource(R.string.network_type),
options = getNetworkTypeDisplayNames(),
selectedOptionsState = networkTypeSelectedOptionsState,
emptyVal = stringResource(R.string.network_type_unspecified),
enabled = apnData.networkTypeEnabled
) {}
}
}
}

View File

@@ -29,6 +29,12 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import com.android.internal.util.ArrayUtils
import com.android.settings.R
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkType
import com.android.settings.network.apn.ApnTypes.APN_TYPES
import com.android.settings.network.apn.ApnTypes.APN_TYPE_ALL
import com.android.settings.network.apn.ApnTypes.APN_TYPE_EMERGENCY
import com.android.settings.network.apn.ApnTypes.APN_TYPE_IA
import com.android.settings.network.apn.ApnTypes.APN_TYPE_IMS
import com.android.settings.network.apn.ApnTypes.APN_TYPE_MCX
import java.util.Locale
data class ApnData(
@@ -113,67 +119,6 @@ data class CustomizedConfig(
val defaultApnRoamingProtocol: String = "",
)
/**
* APN types for data connections. These are usage categories for an APN
* entry. One APN entry may support multiple APN types, eg, a single APN
* may service regular internet traffic ("default") as well as MMS-specific
* connections.<br></br>
* APN_TYPE_ALL is a special type to indicate that this APN entry can
* service all data connections.
*/
const val APN_TYPE_ALL = "*"
/** APN type for default data traffic */
const val APN_TYPE_DEFAULT = "default"
/** APN type for MMS traffic */
const val APN_TYPE_MMS = "mms"
/** APN type for SUPL assisted GPS */
const val APN_TYPE_SUPL = "supl"
/** APN type for DUN traffic */
const val APN_TYPE_DUN = "dun"
/** APN type for HiPri traffic */
const val APN_TYPE_HIPRI = "hipri"
/** APN type for FOTA */
const val APN_TYPE_FOTA = "fota"
/** APN type for IMS */
const val APN_TYPE_IMS = "ims"
/** APN type for CBS */
const val APN_TYPE_CBS = "cbs"
/** APN type for IA Initial Attach APN */
const val APN_TYPE_IA = "ia"
/** APN type for Emergency PDN. This is not an IA apn, but is used
* for access to carrier services in an emergency call situation. */
const val APN_TYPE_EMERGENCY = "emergency"
/** APN type for Mission Critical Services */
const val APN_TYPE_MCX = "mcx"
/** APN type for XCAP */
const val APN_TYPE_XCAP = "xcap"
val APN_TYPES = arrayOf(
APN_TYPE_DEFAULT,
APN_TYPE_MMS,
APN_TYPE_SUPL,
APN_TYPE_DUN,
APN_TYPE_HIPRI,
APN_TYPE_FOTA,
APN_TYPE_IMS,
APN_TYPE_CBS,
APN_TYPE_IA,
APN_TYPE_EMERGENCY,
APN_TYPE_MCX,
APN_TYPE_XCAP
)
/**
* Initialize ApnData according to the arguments.
* @param arguments The data passed in when the user calls PageProvider.
@@ -483,25 +428,6 @@ fun hasAllApns(apnTypes: List<String>): Boolean {
private fun normalizeApnType(apnType: String): String =
apnType.trim().lowercase(Locale.getDefault())
fun updateApnType(apnData: ApnData): String {
return if (apnData.apnType == "" && apnData.customizedConfig.defaultApnTypes.isNotEmpty())
getEditableApnType(apnData)
else
apnData.apnType
}
private fun getEditableApnType(apnData: ApnData): String {
val customizedConfig = apnData.customizedConfig
return customizedConfig.defaultApnTypes.filterNot { apnType ->
customizedConfig.readOnlyApnTypes.contains(apnType) || apnType in listOf(
APN_TYPE_IA,
APN_TYPE_EMERGENCY,
APN_TYPE_MCX,
APN_TYPE_IMS,
)
}.joinToString()
}
fun deleteApn(uri: Uri, context: Context) {
val contentResolver = context.contentResolver
contentResolver.delete(uri, null, null)

View File

@@ -0,0 +1,143 @@
/*
* 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.apn
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
object ApnTypes {
/**
* APN types for data connections. These are usage categories for an APN
* entry. One APN entry may support multiple APN types, eg, a single APN
* may service regular internet traffic ("default") as well as MMS-specific
* connections.<br></br>
* APN_TYPE_ALL is a special type to indicate that this APN entry can
* service all data connections.
*/
const val APN_TYPE_ALL = "*"
/** APN type for default data traffic */
const val APN_TYPE_DEFAULT = "default"
/** APN type for MMS traffic */
const val APN_TYPE_MMS = "mms"
/** APN type for SUPL assisted GPS */
const val APN_TYPE_SUPL = "supl"
/** APN type for DUN traffic */
const val APN_TYPE_DUN = "dun"
/** APN type for HiPri traffic */
const val APN_TYPE_HIPRI = "hipri"
/** APN type for FOTA */
const val APN_TYPE_FOTA = "fota"
/** APN type for IMS */
const val APN_TYPE_IMS = "ims"
/** APN type for CBS */
const val APN_TYPE_CBS = "cbs"
/** APN type for IA Initial Attach APN */
const val APN_TYPE_IA = "ia"
/** APN type for Emergency PDN. This is not an IA apn, but is used
* for access to carrier services in an emergency call situation. */
const val APN_TYPE_EMERGENCY = "emergency"
/** APN type for Mission Critical Services */
const val APN_TYPE_MCX = "mcx"
/** APN type for XCAP */
const val APN_TYPE_XCAP = "xcap"
/** APN type for VSIM */
const val APN_TYPE_VSIM = "vsim"
/** APN type for BIP */
const val APN_TYPE_BIP = "bip"
/** APN type for ENTERPRISE */
const val APN_TYPE_ENTERPRISE = "enterprise"
val APN_TYPES = arrayOf(
APN_TYPE_DEFAULT,
APN_TYPE_MMS,
APN_TYPE_SUPL,
APN_TYPE_DUN,
APN_TYPE_HIPRI,
APN_TYPE_FOTA,
APN_TYPE_IMS,
APN_TYPE_CBS,
APN_TYPE_IA,
APN_TYPE_EMERGENCY,
APN_TYPE_MCX,
APN_TYPE_XCAP,
APN_TYPE_VSIM,
APN_TYPE_BIP,
APN_TYPE_ENTERPRISE
)
val APN_TYPES_OPTIONS = listOf(APN_TYPE_ALL) + APN_TYPES
fun getApnTypeSelectedOptionsState(apnType: String): SnapshotStateList<Int> {
val apnTypeSelectedOptionsState = mutableStateListOf<Int>()
if (apnType.contains(APN_TYPE_ALL))
APN_TYPES_OPTIONS.forEachIndexed { index, _ ->
apnTypeSelectedOptionsState.add(index)
}
else {
APN_TYPES_OPTIONS.forEachIndexed { index, type ->
if (apnType.contains(type)) {
apnTypeSelectedOptionsState.add(index)
}
}
if (apnTypeSelectedOptionsState.size == APN_TYPES.size)
apnTypeSelectedOptionsState.add(APN_TYPES_OPTIONS.indexOf(APN_TYPE_ALL))
}
return apnTypeSelectedOptionsState
}
fun updateApnType(
apnTypeSelectedOptionsState: SnapshotStateList<Int>,
defaultApnTypes: List<String>,
readOnlyApnTypes: List<String>
): String {
val apnType = apnTypeSelectedOptionsState.joinToString { APN_TYPES_OPTIONS[it] }
if (apnType.contains(APN_TYPE_ALL)) return APN_TYPE_ALL
return if (apnType == "" && defaultApnTypes.isNotEmpty())
getEditableApnType(defaultApnTypes, readOnlyApnTypes)
else
apnType
}
private fun getEditableApnType(
defaultApnTypes: List<String>,
readOnlyApnTypes: List<String>
): String {
return defaultApnTypes.filterNot { apnType ->
readOnlyApnTypes.contains(apnType) || apnType in listOf(
APN_TYPE_IA,
APN_TYPE_EMERGENCY,
APN_TYPE_MCX,
APN_TYPE_IMS,
)
}.joinToString()
}
}

View File

@@ -19,7 +19,9 @@ package com.android.settings.notification.zen;
import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.app.NotificationManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -100,10 +102,21 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase {
public boolean onPreferenceClick(Preference preference) {
Bundle bundle = new Bundle();
bundle.putString(ZenCustomRuleSettings.RULE_ID, mId);
// When modes_api flag is on, we skip the radio button screen distinguishing
// between "default" and "custom" and take users directly to the custom
// settings screen.
String destination = ZenCustomRuleSettings.class.getName();
int sourceMetricsCategory = 0;
if (Flags.modesApi()) {
// From ZenRuleCustomPolicyPreferenceController#launchCustomSettings
destination = ZenCustomRuleConfigSettings.class.getName();
sourceMetricsCategory = SettingsEnums.ZEN_CUSTOM_RULE_SOUND_SETTINGS;
}
new SubSettingLauncher(mContext)
.setDestination(ZenCustomRuleSettings.class.getName())
.setDestination(destination)
.setArguments(bundle)
.setSourceMetricsCategory(0) // TODO
.setSourceMetricsCategory(sourceMetricsCategory)
.launch();
return true;
}

View File

@@ -26,7 +26,6 @@ import static com.android.settings.privatespace.PrivateSpaceSetupActivity.ACCOUN
import static com.android.settings.privatespace.PrivateSpaceSetupActivity.EXTRA_ACTION_TYPE;
import static com.android.settings.privatespace.PrivateSpaceSetupActivity.SET_LOCK_ACTION;
import android.app.KeyguardManager;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -37,6 +36,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import com.android.internal.widget.LockPatternUtils;
import com.android.settings.R;
import com.android.settings.SetupWizardUtils;
import com.android.settings.overlay.FeatureFactory;
@@ -52,9 +52,10 @@ public class PrivateProfileContextHelperActivity extends FragmentActivity {
private final ActivityResultLauncher<Intent> mAddAccountToPrivateProfile =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), this::onAccountAdded);
private final ActivityResultLauncher<Intent> mVerifyDeviceLock =
private final ActivityResultLauncher<Intent> mSetNewPrivateProfileLock =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), this::onSetDeviceNewLock);
new ActivityResultContracts.StartActivityForResult(),
this::onSetNewProfileLockActionCompleted);
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -88,7 +89,7 @@ public class PrivateProfileContextHelperActivity extends FragmentActivity {
intent.putExtra(
EXTRA_KEY_CHOOSE_LOCK_SCREEN_DESCRIPTION,
R.string.private_space_lock_setup_description);
mVerifyDeviceLock.launch(intent);
mSetNewPrivateProfileLock.launch(intent);
}
private void onAccountAdded(@Nullable ActivityResult result) {
@@ -102,10 +103,12 @@ public class PrivateProfileContextHelperActivity extends FragmentActivity {
finish();
}
private void onSetDeviceNewLock(@Nullable ActivityResult result) {
// TODO(b/307281644) : Verify this for biometrics and check result code after new
// Authentication changes are merged.
if (result != null && getSystemService(KeyguardManager.class).isDeviceSecure()) {
private void onSetNewProfileLockActionCompleted(@Nullable ActivityResult result) {
LockPatternUtils lockPatternUtils =
FeatureFactory.getFeatureFactory()
.getSecurityFeatureProvider()
.getLockPatternUtils(this);
if (result != null && lockPatternUtils.isSeparateProfileChallengeEnabled(getUserId())) {
Log.i(TAG, "separate private space lock setup success");
setResult(RESULT_OK);
} else {

View File

@@ -47,6 +47,7 @@ import com.android.settings.spa.development.UsageStatsPageProvider
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
import com.android.settings.spa.home.HomePageProvider
import com.android.settings.spa.network.NetworkAndInternetPageProvider
import com.android.settings.spa.network.SimOnboardingPageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.notification.NotificationMainPageProvider
import com.android.settings.spa.system.AppLanguagesPageProvider
@@ -114,6 +115,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
StorageAppListPageProvider.Apps,
StorageAppListPageProvider.Games,
ApnEditPageProvider,
SimOnboardingPageProvider,
)
override val logger = if (FeatureFlagUtils.isEnabled(

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SignalCellularAlt
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.dialog.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter
import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.BottomAppBarButton
import com.android.settingslib.spa.widget.scaffold.SuwScaffold
import com.android.settingslib.spa.widget.ui.SettingsBody
/**
* the sim onboarding label compose
*/
@Composable
fun SimOnboardingLabelSimImpl(
nextAction: () -> Unit,
cancelAction: () -> Unit,
onboardingService: SimOnboardingService
) {
SuwScaffold(
imageVector = Icons.Outlined.SignalCellularAlt,
title = stringResource(R.string.sim_onboarding_label_sim_title),
actionButton = BottomAppBarButton(
stringResource(R.string.sim_onboarding_next),
nextAction
),
dismissButton = BottomAppBarButton(
stringResource(R.string.cancel),
cancelAction
),
) {
labelSimBody(onboardingService)
}
}
@Composable
private fun labelSimBody(onboardingService: SimOnboardingService) {
Column(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsBody(stringResource(R.string.sim_onboarding_label_sim_msg))
}
for (subInfo in onboardingService.getSelectableSubscriptionInfo()) {
var titleSimName by remember {
mutableStateOf(
onboardingService.getSubscriptionInfoDisplayName(subInfo)
)
}
var summaryNumber = subInfo.number
// TODO using the SubscriptionUtil.getFormattedPhoneNumber
val alertDialogPresenter = rememberAlertDialogPresenter(
confirmButton = AlertDialogButton(
stringResource(R.string.mobile_network_sim_name_rename)
) {
onboardingService.addItemForRenaming(subInfo, titleSimName)
},
dismissButton = AlertDialogButton(stringResource(R.string.cancel)) {
titleSimName = onboardingService.getSubscriptionInfoDisplayName(subInfo)
},
title = stringResource(R.string.sim_onboarding_label_sim_dialog_title),
text = {
Text(summaryNumber)
SettingsOutlinedTextField(
value = titleSimName,
label = stringResource(R.string.sim_onboarding_label_sim_dialog_label),
enabled = true
) {
titleSimName = it
}
},
)
Preference(object : PreferenceModel {
override val title = titleSimName
override val summary: () -> String
get() = { summaryNumber }
override val onClick = alertDialogPresenter::open
})
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
const val SUB_ID = "subId"
enum class SimOnboardingScreen(val stringResId: Int) {
LabelSim(R.string.sim_onboarding_label_sim_title),
SelectSim(R.string.sim_onboarding_select_sim_title),
PrimarySim(R.string.sim_onboarding_primary_sim_title)
}
/**
* Showing the sim onboarding which is the process flow of sim switching on.
*/
object SimOnboardingPageProvider : SettingsPageProvider {
override val name = "SimOnboardingPageProvider"
override val parameter = listOf(
navArgument(SUB_ID) { type = NavType.IntType },
)
private val owner = createSettingsPage()
@VisibleForTesting
var onboardingService: SimOnboardingService = SimOnboardingService()
fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
.setUiLayoutFn {
// never using
Preference(object : PreferenceModel {
override val title = name
override val onClick = navigator(getRoute(-1))
})
}
@Composable
override fun Page(arguments: Bundle?) {
initServiceData(arguments!!.getInt(SUB_ID))
PageImpl(onboardingService,rememberNavController())
}
fun getRoute(
subId: Int
): String = "${name}/$subId"
@Composable
fun initServiceData(targetSubId: Int) {
onboardingService.initData(targetSubId, LocalContext.current)
}
}
private fun Context.getActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
@Composable
fun PageImpl(onboardingService:SimOnboardingService,navHostController: NavHostController) {
val context = LocalContext.current
var previousPageOfOnboarding: () -> Unit = { context.getActivity()?.finish() }
NavHost(
navController = navHostController,
startDestination = SimOnboardingScreen.LabelSim.name
) {
composable(route = SimOnboardingScreen.LabelSim.name) {
val nextPage =
// Adding more conditions
if (onboardingService.isMultipleEnabledProfilesSupported) {
SimOnboardingScreen.SelectSim.name
} else {
SimOnboardingScreen.PrimarySim.name
}
SimOnboardingLabelSimImpl(
nextAction = { navHostController.navigate(nextPage) },
cancelAction = previousPageOfOnboarding,
onboardingService = onboardingService
)
}
composable(route = SimOnboardingScreen.PrimarySim.name) {
SimOnboardingPrimarySimImpl(
nextAction = {
//go back and activate sim
},
cancelAction = previousPageOfOnboarding,
onboardingService = onboardingService
)
}
composable(route = SimOnboardingScreen.SelectSim.name) {
SimOnboardingSelectSimImpl(
nextAction = { navHostController.navigate(SimOnboardingScreen.PrimarySim.name) },
cancelAction = previousPageOfOnboarding,
onboardingService = onboardingService
)
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Message
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.material.icons.outlined.SignalCellularAlt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.ListPreference
import com.android.settingslib.spa.widget.preference.ListPreferenceModel
import com.android.settingslib.spa.widget.preference.ListPreferenceOption
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.scaffold.BottomAppBarButton
import com.android.settingslib.spa.widget.scaffold.SuwScaffold
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsIcon
/**
* the sim onboarding primary sim compose
*/
@Composable
fun SimOnboardingPrimarySimImpl(
nextAction: () -> Unit,
cancelAction: () -> Unit,
onboardingService: SimOnboardingService
) {
SuwScaffold(
imageVector = Icons.Outlined.SignalCellularAlt,
title = stringResource(id = R.string.sim_onboarding_primary_sim_title),
actionButton = BottomAppBarButton(
stringResource(id = R.string.done),
nextAction
),
dismissButton = BottomAppBarButton(
stringResource(id = R.string.cancel),
cancelAction
),
) {
primarySimBody(onboardingService)
}
}
@Composable
private fun primarySimBody(onboardingService: SimOnboardingService) {
//TODO: Load the status from the frameworks
var callsSelectedId = rememberSaveable { mutableIntStateOf(1) }
var textsSelectedId = rememberSaveable { mutableIntStateOf(1) }
var mobileDataSelectedId = rememberSaveable { mutableIntStateOf(1) }
var automaticDataChecked by rememberSaveable { mutableStateOf(true) }
Column(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsBody(stringResource(id = R.string.sim_onboarding_primary_sim_msg))
}
var selectableSubscriptionInfo = onboardingService.getSelectableSubscriptionInfo()
var list = listOf(ListPreferenceOption(id = -1, text = "Loading"))
if (selectableSubscriptionInfo.size >= 2) {
list = listOf(
ListPreferenceOption(
id = selectableSubscriptionInfo[0].subscriptionId,
text = "${selectableSubscriptionInfo[0].displayName}"
),
ListPreferenceOption(
id = selectableSubscriptionInfo[1].subscriptionId,
text = "${selectableSubscriptionInfo[1].displayName}"
),
ListPreferenceOption(
id = -1,
text = stringResource(id = R.string.sim_calls_ask_first_prefs_title)
),
)
} else {
// set all of primary sim items' enable as false and showing that sim.
}
createPrimarySimListPreference(
stringResource(id = R.string.primary_sim_calls_title),
list,
callsSelectedId,
ImageVector.vectorResource(R.drawable.ic_phone),
true
)
createPrimarySimListPreference(
stringResource(id = R.string.primary_sim_texts_title),
list,
textsSelectedId,
Icons.AutoMirrored.Outlined.Message,
true
)
createPrimarySimListPreference(
stringResource(id = R.string.mobile_data_settings_title),
list,
mobileDataSelectedId,
Icons.Outlined.DataUsage,
true
)
val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title)
val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg)
SwitchPreference(remember {
object : SwitchPreferenceModel {
override val title = autoDataTitle
override val summary = { autoDataSummary }
override val checked = { automaticDataChecked }
override val onCheckedChange =
{ newChecked: Boolean -> automaticDataChecked = newChecked }
}
})
}
@Composable
fun createPrimarySimListPreference(
title: String,
list: List<ListPreferenceOption>,
selectedId: MutableIntState,
icon: ImageVector,
enable: Boolean
) = ListPreference(remember {
object : ListPreferenceModel {
override val title = title
override val options = list
override val selectedId = selectedId
override val onIdSelected: (id: Int) -> Unit = { selectedId.intValue = it }
override val icon = @Composable {
SettingsIcon(icon)
}
override val enabled: () -> Boolean
get() = { enable }
}
})

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SignalCellularAlt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.CheckboxPreference
import com.android.settingslib.spa.widget.preference.CheckboxPreferenceModel
import com.android.settingslib.spa.widget.scaffold.BottomAppBarButton
import com.android.settingslib.spa.widget.scaffold.SuwScaffold
import com.android.settingslib.spa.widget.ui.SettingsBody
/**
* the sim onboarding select sim compose
*/
@Composable
fun SimOnboardingSelectSimImpl(
nextAction: () -> Unit,
cancelAction: () -> Unit,
onboardingService: SimOnboardingService
) {
SuwScaffold(
imageVector = Icons.Outlined.SignalCellularAlt,
title = stringResource(id = R.string.sim_onboarding_select_sim_title),
actionButton = BottomAppBarButton(
stringResource(id = R.string.sim_onboarding_next),
nextAction
),
dismissButton = BottomAppBarButton(
stringResource(id = R.string.cancel),
cancelAction
),
) {
selectSimBody(onboardingService)
}
}
@Composable
private fun selectSimBody(onboardingService: SimOnboardingService) {
Column(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsBody(stringResource(id = R.string.sim_onboarding_select_sim_msg))
}
for (subInfo in onboardingService.getSelectableSubscriptionInfo()) {
var title = onboardingService.getSubscriptionInfoDisplayName(subInfo)
var summaryNumber =
subInfo.number // TODO using the SubscriptionUtil.getFormattedPhoneNumber
var changeable = subInfo.isActive
var checked by rememberSaveable { mutableStateOf(!subInfo.isActive) }
CheckboxPreference(remember {
object : CheckboxPreferenceModel {
override val title = title
override val summary: () -> String
get() = { summaryNumber }
override val checked = { checked }
override val changeable = { changeable }
override val onCheckedChange = { newChecked: Boolean -> checked = newChecked }
}
})
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.bluetooth;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -28,23 +29,33 @@ import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import android.sysprop.BluetoothProperties;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
import androidx.preference.SwitchPreferenceCompat;
import com.android.settings.flags.Flags;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowBluetoothDevice;
import com.android.settingslib.R;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.MapProfile;
import com.android.settingslib.bluetooth.PbapServerProfile;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -59,30 +70,41 @@ import java.util.Map;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@Ignore
@Config(shadows = ShadowBluetoothDevice.class)
public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String LE_DEVICE_MODEL = "le_audio_headset";
private static final String NON_LE_DEVICE_MODEL = "non_le_audio_headset";
private BluetoothDetailsProfilesController mController;
private List<LocalBluetoothProfile> mConnectableProfiles;
private PreferenceCategory mProfiles;
private BluetoothFeatureProvider mFeatureProvider;
@Mock
private LocalBluetoothManager mLocalManager;
@Mock
private LocalBluetoothProfileManager mProfileManager;
@Mock
private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
@Override
public void setUp() {
super.setUp();
FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
mFeatureProvider = fakeFeatureFactory.getBluetoothFeatureProvider();
mProfiles = spy(new PreferenceCategory(mContext));
when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager);
mConnectableProfiles = new ArrayList<>();
when(mLocalManager.getProfileManager()).thenReturn(mProfileManager);
when(mLocalManager.getCachedDeviceManager()).thenReturn(mCachedBluetoothDeviceManager);
when(mCachedBluetoothDeviceManager.getCachedDevicesCopy())
.thenReturn(ImmutableList.of(mCachedDevice));
when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation ->
new ArrayList<>(mConnectableProfiles)
);
@@ -196,25 +218,26 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
return profile;
}
/** Returns the list of SwitchPreference objects added to the screen - there should be one per
* Bluetooth profile.
/**
* Returns the list of SwitchPreferenceCompat objects added to the screen - there should be one
* per Bluetooth profile.
*/
private List<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
private List<SwitchPreferenceCompat> getProfileSwitches(boolean expectOnlyMConnectable) {
if (expectOnlyMConnectable) {
assertThat(mConnectableProfiles).isNotEmpty();
assertThat(mProfiles.getPreferenceCount() - 1).isEqualTo(mConnectableProfiles.size());
}
List<SwitchPreference> result = new ArrayList<>();
List<SwitchPreferenceCompat> result = new ArrayList<>();
for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
final Preference preference = mProfiles.getPreference(i);
if (preference instanceof SwitchPreference) {
result.add((SwitchPreference) preference);
if (preference instanceof SwitchPreferenceCompat) {
result.add((SwitchPreferenceCompat) preference);
}
}
return result;
}
private void verifyProfileSwitchTitles(List<SwitchPreference> switches) {
private void verifyProfileSwitchTitles(List<SwitchPreferenceCompat> switches) {
for (int i = 0; i < switches.size(); i++) {
String expectedTitle =
mContext.getString(mConnectableProfiles.get(i).getNameResource(mDevice));
@@ -234,7 +257,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_a2dp, true);
addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_headset, false);
showScreen(mController);
List<SwitchPreference> switches = getProfileSwitches(true);
List<SwitchPreferenceCompat> switches = getProfileSwitches(true);
verifyProfileSwitchTitles(switches);
assertThat(switches.get(0).isChecked()).isTrue();
assertThat(switches.get(1).isChecked()).isFalse();
@@ -260,8 +283,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_a2dp, true);
addFakeProfile(com.android.settingslib.R.string.bluetooth_profile_headset, true);
showScreen(mController);
List<SwitchPreference> switches = getProfileSwitches(true);
SwitchPreference pref = switches.get(0);
List<SwitchPreferenceCompat> switches = getProfileSwitches(true);
SwitchPreferenceCompat pref = switches.get(0);
// Clicking the pref should cause the profile to become not-preferred.
assertThat(pref.isChecked()).isTrue();
@@ -296,14 +319,16 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
PbapServerProfile psp = mock(PbapServerProfile.class);
when(psp.getNameResource(mDevice))
.thenReturn(com.android.settingslib.R.string.bluetooth_profile_pbap);
when(psp.getSummaryResourceForDevice(mDevice))
.thenReturn(R.string.bluetooth_profile_pbap_summary);
when(psp.toString()).thenReturn(PbapServerProfile.NAME);
when(psp.isProfileReady()).thenReturn(true);
when(mProfileManager.getPbapProfile()).thenReturn(psp);
showScreen(mController);
List<SwitchPreference> switches = getProfileSwitches(false);
List<SwitchPreferenceCompat> switches = getProfileSwitches(false);
assertThat(switches.size()).isEqualTo(1);
SwitchPreference pref = switches.get(0);
SwitchPreferenceCompat pref = switches.get(0);
assertThat(pref.getTitle()).isEqualTo(
mContext.getString(com.android.settingslib.R.string.bluetooth_profile_pbap));
assertThat(pref.isChecked()).isTrue();
@@ -321,14 +346,16 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
PbapServerProfile psp = mock(PbapServerProfile.class);
when(psp.getNameResource(mDevice))
.thenReturn(com.android.settingslib.R.string.bluetooth_profile_pbap);
when(psp.getSummaryResourceForDevice(mDevice))
.thenReturn(R.string.bluetooth_profile_pbap_summary);
when(psp.toString()).thenReturn(PbapServerProfile.NAME);
when(psp.isProfileReady()).thenReturn(true);
when(mProfileManager.getPbapProfile()).thenReturn(psp);
showScreen(mController);
List<SwitchPreference> switches = getProfileSwitches(false);
List<SwitchPreferenceCompat> switches = getProfileSwitches(false);
assertThat(switches.size()).isEqualTo(1);
SwitchPreference pref = switches.get(0);
SwitchPreferenceCompat pref = switches.get(0);
assertThat(pref.getTitle()).isEqualTo(
mContext.getString(com.android.settingslib.R.string.bluetooth_profile_pbap));
assertThat(pref.isChecked()).isFalse();
@@ -350,9 +377,9 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile);
mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
showScreen(mController);
List<SwitchPreference> switches = getProfileSwitches(false);
List<SwitchPreferenceCompat> switches = getProfileSwitches(false);
assertThat(switches.size()).isEqualTo(1);
SwitchPreference pref = switches.get(0);
SwitchPreferenceCompat pref = switches.get(0);
assertThat(pref.getTitle()).isEqualTo(
mContext.getString(com.android.settingslib.R.string.bluetooth_profile_map));
assertThat(pref.isChecked()).isFalse();
@@ -379,8 +406,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
return profile;
}
private SwitchPreference getHighQualityAudioPref() {
return (SwitchPreference) mScreen.findPreference(
private SwitchPreferenceCompat getHighQualityAudioPref() {
return (SwitchPreferenceCompat) mScreen.findPreference(
BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
}
@@ -389,7 +416,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
setupDevice(makeDefaultDeviceConfig());
addMockA2dpProfile(true, true, true);
showScreen(mController);
SwitchPreference pref = getHighQualityAudioPref();
SwitchPreferenceCompat pref = getHighQualityAudioPref();
assertThat(pref.getKey()).isEqualTo(
BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
@@ -407,7 +434,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
addMockA2dpProfile(true, false, false);
showScreen(mController);
assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0);
SwitchPreferenceCompat pref = (SwitchPreferenceCompat) mProfiles.getPreference(0);
assertThat(pref.getKey())
.isNotEqualTo(BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
assertThat(pref.getTitle()).isEqualTo(
@@ -420,7 +447,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
addMockA2dpProfile(true, true, true);
when(mCachedDevice.isBusy()).thenReturn(true);
showScreen(mController);
SwitchPreference pref = getHighQualityAudioPref();
SwitchPreferenceCompat pref = getHighQualityAudioPref();
assertThat(pref.isEnabled()).isFalse();
}
@@ -433,14 +460,14 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
// Disabling media audio should cause the high quality audio switch to disappear, but not
// the regular audio one.
SwitchPreference audioPref =
(SwitchPreference) mScreen.findPreference(audioProfile.toString());
SwitchPreferenceCompat audioPref =
(SwitchPreferenceCompat) mScreen.findPreference(audioProfile.toString());
audioPref.performClick();
verify(audioProfile).setEnabled(mDevice, false);
when(audioProfile.isEnabled(mDevice)).thenReturn(false);
mController.onDeviceAttributesChanged();
assertThat(audioPref.isVisible()).isTrue();
SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
SwitchPreferenceCompat highQualityAudioPref = getHighQualityAudioPref();
assertThat(highQualityAudioPref.isVisible()).isFalse();
// And re-enabling media audio should make high quality switch to reappear.
@@ -457,8 +484,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
setupDevice(makeDefaultDeviceConfig());
A2dpProfile audioProfile = addMockA2dpProfile(false, true, true);
showScreen(mController);
SwitchPreference audioPref = mScreen.findPreference(audioProfile.toString());
SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
SwitchPreferenceCompat audioPref = mScreen.findPreference(audioProfile.toString());
SwitchPreferenceCompat highQualityAudioPref = getHighQualityAudioPref();
assertThat(audioPref).isNotNull();
assertThat(audioPref.isChecked()).isFalse();
assertThat(highQualityAudioPref).isNotNull();
@@ -489,4 +516,46 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont
assertThat(mController.isModelNameInAllowList(null)).isFalse();
assertThat(mController.isModelNameInAllowList(NON_LE_DEVICE_MODEL)).isFalse();
}
@Test
public void prefKeyInBlockingList_hideToggle() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_BLUETOOTH_PROFILE_TOGGLE_VISIBILITY_CHECKER);
setupDevice(makeDefaultDeviceConfig());
LeAudioProfile leAudioProfile = mock(LeAudioProfile.class);
when(leAudioProfile.getNameResource(mDevice))
.thenReturn(com.android.settingslib.R.string.bluetooth_profile_le_audio);
when(leAudioProfile.isProfileReady()).thenReturn(true);
when(leAudioProfile.toString()).thenReturn("LE_AUDIO");
when(mProfileManager.getLeAudioProfile()).thenReturn(leAudioProfile);
when(mFeatureProvider.getInvisibleProfilePreferenceKeys(any(), any()))
.thenReturn(ImmutableSet.of("LE_AUDIO"));
mConnectableProfiles.add(leAudioProfile);
showScreen(mController);
List<SwitchPreferenceCompat> switches = getProfileSwitches(false);
assertThat(switches.get(0).isVisible()).isFalse();
}
@Test
public void prefKeyNotInBlockingList_showToggle() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_BLUETOOTH_PROFILE_TOGGLE_VISIBILITY_CHECKER);
setupDevice(makeDefaultDeviceConfig());
LeAudioProfile leAudioProfile = mock(LeAudioProfile.class);
when(leAudioProfile.getNameResource(mDevice))
.thenReturn(com.android.settingslib.R.string.bluetooth_profile_le_audio);
when(leAudioProfile.isProfileReady()).thenReturn(true);
when(leAudioProfile.toString()).thenReturn("LE_AUDIO");
when(mProfileManager.getLeAudioProfile()).thenReturn(leAudioProfile);
when(mFeatureProvider.getInvisibleProfilePreferenceKeys(any(), any()))
.thenReturn(ImmutableSet.of("A2DP"));
mConnectableProfiles.add(leAudioProfile);
showScreen(mController);
List<SwitchPreferenceCompat> switches = getProfileSwitches(false);
assertThat(switches.get(0).isVisible()).isTrue();
}
}

View File

@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.UserInfo;
import android.content.pm.UserProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.widget.TextView;
@@ -42,16 +43,19 @@ import com.android.settingslib.drawer.Tile;
import com.google.android.collect.Lists;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class ProfileSelectDialogTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final UserHandle NORMAL_USER = new UserHandle(1111);
private static final UserHandle REMOVED_USER = new UserHandle(2222);
@@ -67,11 +71,12 @@ public class ProfileSelectDialogTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
final UserInfo userInfo = new UserInfo(
NORMAL_USER.getIdentifier(), "test_user", UserInfo.FLAG_RESTRICTED);
when(mUserManager.getUserInfo(NORMAL_USER.getIdentifier())).thenReturn(userInfo);
final UserProperties userProperties = new UserProperties.Builder().build();
when(mUserManager.getUserProperties(NORMAL_USER)).thenReturn(userProperties);
mActivityInfo = new ActivityInfo();
mActivityInfo.packageName = "pkg";
mActivityInfo.name = "cls";
@@ -89,7 +94,6 @@ public class ProfileSelectDialogTest {
verify(mUserManager, never()).getUserInfo(NORMAL_USER.getIdentifier());
}
@Ignore("b/313569889")
@Test
public void updateUserHandlesIfNeeded_Remove() {
final Tile tile = new ActivityTile(mActivityInfo, CategoryKey.CATEGORY_HOMEPAGE);
@@ -105,7 +109,6 @@ public class ProfileSelectDialogTest {
verify(mUserManager, times(2)).getUserInfo(REMOVED_USER.getIdentifier());
}
@Ignore("b/313569889")
@Test
public void updateUserHandlesIfNeeded_removesCloneProfile() {
final UserInfo userInfo = new UserInfo(CLONE_USER.getIdentifier(), "clone_user", null,
@@ -122,7 +125,6 @@ public class ProfileSelectDialogTest {
verify(mUserManager, times(1)).getUserInfo(CLONE_USER.getIdentifier());
}
@Ignore("b/313569889")
@Test
public void updatePendingIntentsIfNeeded_removesUsersWithNoPendingIntentsAndCloneProfile() {
final UserInfo userInfo = new UserInfo(CLONE_USER.getIdentifier(), "clone_user", null,

View File

@@ -50,9 +50,9 @@ class ApnEditPageProviderTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val apnName = "apn_name"
private val mmsc = "mmsc"
private val mmsProxy = "mms_proxy"
private val apnType = "apn_type"
private val proxy = "proxy"
private val port = "port"
private val apnType = context.resources.getString(R.string.apn_type)
private val apnRoaming = "IPv4"
private val apnEnable = context.resources.getString(R.string.carrier_enabled)
private val apnProtocolOptions =
@@ -61,8 +61,8 @@ class ApnEditPageProviderTest {
private val passwordTitle = context.resources.getString(R.string.apn_password)
private val apnInit = ApnData(
name = apnName,
mmsc = mmsc,
mmsProxy = mmsProxy,
proxy = proxy,
port = port,
apnType = apnType,
apnRoaming = apnProtocolOptions.indexOf(apnRoaming),
apnEnable = true
@@ -94,23 +94,23 @@ class ApnEditPageProviderTest {
}
@Test
fun mmsc_displayed() {
fun proxy_displayed() {
composeTestRule.setContent {
ApnPage(apnInit, remember { apnData }, uri)
}
composeTestRule.onRoot().onChild().onChildAt(0)
.performScrollToNode(hasText(mmsc, true))
composeTestRule.onNodeWithText(mmsc, true).assertIsDisplayed()
.performScrollToNode(hasText(proxy, true))
composeTestRule.onNodeWithText(proxy, true).assertIsDisplayed()
}
@Test
fun mms_proxy_displayed() {
fun port_displayed() {
composeTestRule.setContent {
ApnPage(apnInit, remember { apnData }, uri)
}
composeTestRule.onRoot().onChild().onChildAt(0)
.performScrollToNode(hasText(mmsProxy, true))
composeTestRule.onNodeWithText(mmsProxy, true).assertIsDisplayed()
.performScrollToNode(hasText(port, true))
composeTestRule.onNodeWithText(port, true).assertIsDisplayed()
}
@Test

View File

@@ -0,0 +1,197 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import android.content.Context
import android.telephony.SubscriptionInfo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class SimOnboardingLabelSimTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private var mockSimOnboardingService = mock<SimOnboardingService> {
on { targetSubId }.doReturn(-1)
on { targetSubInfo }.doReturn(null)
on { availableSubInfoList }.doReturn(listOf())
on { activeSubInfoList }.doReturn(listOf())
on { slotInfoList }.doReturn(listOf())
on { uiccCardInfoList }.doReturn(listOf())
on { selectedSubInfoList }.doReturn(mutableListOf())
on { targetPrimarySimCalls }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimTexts }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimMobileData }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
}
private val nextAction: () -> Unit = mock()
private val cancelAction: () -> Unit = mock()
@Test
fun simOnboardingLabelSimImpl_showTitle() {
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_label_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingLabelSimImpl_showSubTitle() {
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_label_sim_msg))
.assertIsDisplayed()
}
@Test
fun simOnboardingLabelSimImpl_clickNextAction_verifyNextAction() {
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_next))
.performClick()
verify(nextAction)
}
@Test
fun simOnboardingLabelSimImpl_clickCancelAction_verifyCancelAction() {
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.cancel))
.performClick()
verify(cancelAction)
}
@Test
fun simOnboardingLabelSimImpl_showItem_show3Items() {
mockSimOnboardingService.stub {
on { targetSubId }.doReturn(SUB_ID_1)
on { targetSubInfo }.doReturn(SUB_INFO_1)
on { availableSubInfoList }.doReturn(listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_3))
on { activeSubInfoList }.doReturn(listOf(SUB_INFO_2, SUB_INFO_3))
on { getSelectableSubscriptionInfo() }.doReturn(
listOf(
SUB_INFO_1,
SUB_INFO_2,
SUB_INFO_3
)
)
on { getSubscriptionInfoDisplayName(SUB_INFO_1) }.doReturn(DISPLAY_NAME_1)
on { getSubscriptionInfoDisplayName(SUB_INFO_2) }.doReturn(DISPLAY_NAME_2)
on { getSubscriptionInfoDisplayName(SUB_INFO_3) }.doReturn(DISPLAY_NAME_3)
}
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(DISPLAY_NAME_1).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_1).assertIsDisplayed()
composeTestRule.onNodeWithText(DISPLAY_NAME_2).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_2).assertIsDisplayed()
composeTestRule.onNodeWithText(DISPLAY_NAME_3).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_3).assertIsDisplayed()
}
@Test
fun simOnboardingLabelSimImpl_showDialog_checkTitle() {
mockSimOnboardingService.stub {
on { targetSubId }.doReturn(SUB_ID_1)
on { targetSubInfo }.doReturn(SUB_INFO_1)
on { availableSubInfoList }.doReturn(listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_3))
on { activeSubInfoList }.doReturn(listOf(SUB_INFO_2, SUB_INFO_3))
on { getSelectableSubscriptionInfo() }.doReturn(
listOf(
SUB_INFO_1,
SUB_INFO_2,
SUB_INFO_3
)
)
on { getSubscriptionInfoDisplayName(SUB_INFO_1) }.doReturn(DISPLAY_NAME_1)
on { getSubscriptionInfoDisplayName(SUB_INFO_2) }.doReturn(DISPLAY_NAME_2)
on { getSubscriptionInfoDisplayName(SUB_INFO_3) }.doReturn(DISPLAY_NAME_3)
}
composeTestRule.setContent {
SimOnboardingLabelSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(DISPLAY_NAME_1).performClick()
composeTestRule.onNodeWithText(
context.getString(R.string.sim_onboarding_label_sim_dialog_title)
)
.assertIsDisplayed()
}
private companion object {
const val SUB_ID_1 = 1
const val SUB_ID_2 = 2
const val SUB_ID_3 = 3
const val DISPLAY_NAME_1 = "Sub 1"
const val DISPLAY_NAME_2 = "Sub 2"
const val DISPLAY_NAME_3 = "Sub 3"
const val NUMBER_1 = "000000001"
const val NUMBER_2 = "000000002"
const val NUMBER_3 = "000000003"
const val PRIMARY_SIM_ASK_EVERY_TIME = -1
val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setDisplayName(DISPLAY_NAME_1)
setNumber(NUMBER_1)
}.build()
val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
setDisplayName(DISPLAY_NAME_2)
setNumber(NUMBER_2)
}.build()
val SUB_INFO_3: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_3)
setDisplayName(DISPLAY_NAME_3)
setNumber(NUMBER_3)
}.build()
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.navigation.compose.rememberNavController
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
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 SimOnboardingPageProviderTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private var mockSimOnboardingService = mock<SimOnboardingService> {
on { targetSubId }.doReturn(SUB_ID)
on { targetSubInfo }.doReturn(null)
on { availableSubInfoList }.doReturn(listOf())
on { activeSubInfoList }.doReturn(listOf())
on { slotInfoList }.doReturn(listOf())
on { uiccCardInfoList }.doReturn(listOf())
on { selectedSubInfoList }.doReturn(mutableListOf())
on { targetPrimarySimCalls }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimTexts }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimMobileData }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
}
@Test
fun simOnboardingPageProvider_name() {
assertThat(SimOnboardingPageProvider.name).isEqualTo("SimOnboardingPageProvider")
}
@Test
fun simOnboardingPage_labelSim() {
composeTestRule.setContent {
val navHostController = rememberNavController()
PageImpl(mockSimOnboardingService, navHostController)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_label_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingPage_nextAction_fromLabelSimToPrimarySim() {
mockSimOnboardingService.stub {
on { isMultipleEnabledProfilesSupported }.thenReturn(false)
}
composeTestRule.setContent {
val navHostController = rememberNavController()
PageImpl(mockSimOnboardingService, navHostController)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_next))
.performClick()
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_primary_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingPage_nextAction_fromLabelSimToSelectSim() {
mockSimOnboardingService.stub {
on { isMultipleEnabledProfilesSupported }.thenReturn(true)
}
composeTestRule.setContent {
val navHostController = rememberNavController()
PageImpl(mockSimOnboardingService, navHostController)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_next))
.performClick()
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_select_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingPage_nextAction_fromSelectSimToPrimarySim() {
composeTestRule.setContent {
val navHostController = rememberNavController()
PageImpl(mockSimOnboardingService, navHostController)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_next))
.performClick()
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_primary_sim_title))
.assertIsDisplayed()
}
private companion object {
const val SUB_ID = 1
const val PRIMARY_SIM_ASK_EVERY_TIME = -1
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import android.content.Context
import android.telephony.SubscriptionInfo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class SimOnboardingPrimarySimTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private var mockSimOnboardingService = mock<SimOnboardingService> {
on { targetSubId }.doReturn(-1)
on { targetSubInfo }.doReturn(null)
on { availableSubInfoList }.doReturn(listOf())
on { activeSubInfoList }.doReturn(listOf())
on { slotInfoList }.doReturn(listOf())
on { uiccCardInfoList }.doReturn(listOf())
on { selectedSubInfoList }.doReturn(mutableListOf())
on { targetPrimarySimCalls }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimTexts }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimMobileData }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
}
private val nextAction: () -> Unit = mock()
private val cancelAction: () -> Unit = mock()
@Test
fun simOnboardingPrimarySimImpl_showTitle() {
composeTestRule.setContent {
SimOnboardingPrimarySimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_primary_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingPrimarySimImpl_showSubTitle() {
composeTestRule.setContent {
SimOnboardingPrimarySimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_primary_sim_msg))
.assertIsDisplayed()
}
@Test
fun simOnboardingPrimarySimImpl_clickCancelAction_verifyCancelAction() {
composeTestRule.setContent {
SimOnboardingPrimarySimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.cancel))
.performClick()
verify(cancelAction)
}
private companion object {
const val SUB_ID_1 = 1
const val SUB_ID_2 = 2
const val SUB_ID_3 = 3
const val DISPLAY_NAME_1 = "Sub 1"
const val DISPLAY_NAME_2 = "Sub 2"
const val DISPLAY_NAME_3 = "Sub 3"
const val NUMBER_1 = "000000001"
const val NUMBER_2 = "000000002"
const val NUMBER_3 = "000000003"
const val PRIMARY_SIM_ASK_EVERY_TIME = -1
val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setDisplayName(DISPLAY_NAME_1)
setNumber(NUMBER_1)
}.build()
val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
setDisplayName(DISPLAY_NAME_2)
setNumber(NUMBER_2)
}.build()
val SUB_INFO_3: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_3)
setDisplayName(DISPLAY_NAME_3)
setNumber(NUMBER_3)
}.build()
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.network
import android.content.Context
import android.telephony.SubscriptionInfo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.network.SimOnboardingService
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class SimOnboardingSelectSimTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private var mockSimOnboardingService = mock<SimOnboardingService> {
on { targetSubId }.doReturn(-1)
on { targetSubInfo }.doReturn(null)
on { availableSubInfoList }.doReturn(listOf())
on { activeSubInfoList }.doReturn(listOf())
on { slotInfoList }.doReturn(listOf())
on { uiccCardInfoList }.doReturn(listOf())
on { selectedSubInfoList }.doReturn(mutableListOf())
on { targetPrimarySimCalls }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimTexts }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
on { targetPrimarySimMobileData }.doReturn(PRIMARY_SIM_ASK_EVERY_TIME)
}
private val nextAction: () -> Unit = mock()
private val cancelAction: () -> Unit = mock()
@Test
fun simOnboardingSelectSimImpl_showTitle() {
composeTestRule.setContent {
SimOnboardingSelectSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_select_sim_title))
.assertIsDisplayed()
}
@Test
fun simOnboardingSelectSimImpl_showSubTitle() {
composeTestRule.setContent {
SimOnboardingSelectSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_select_sim_msg))
.assertIsDisplayed()
}
@Test
fun simOnboardingSelectSimImpl_clickNextAction_verifyNextAction() {
composeTestRule.setContent {
SimOnboardingSelectSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.sim_onboarding_next))
.performClick()
verify(nextAction)
}
@Test
fun simOnboardingSelectSimImpl_clickCancelAction_verifyCancelAction() {
composeTestRule.setContent {
SimOnboardingSelectSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(context.getString(R.string.cancel))
.performClick()
verify(cancelAction)
}
@Test
fun simOnboardingSelectSimImpl_showItem_show3Items() {
mockSimOnboardingService.stub {
on { targetSubId }.doReturn(SUB_ID_1)
on { targetSubInfo }.doReturn(SUB_INFO_1)
on { availableSubInfoList }.doReturn(listOf(SUB_INFO_1, SUB_INFO_2, SUB_INFO_3))
on { activeSubInfoList }.doReturn(listOf(SUB_INFO_2, SUB_INFO_3))
on { getSelectableSubscriptionInfo() }.doReturn(
listOf(
SUB_INFO_1,
SUB_INFO_2,
SUB_INFO_3
)
)
on { getSubscriptionInfoDisplayName(SUB_INFO_1) }.doReturn(DISPLAY_NAME_1)
on { getSubscriptionInfoDisplayName(SUB_INFO_2) }.doReturn(DISPLAY_NAME_2)
on { getSubscriptionInfoDisplayName(SUB_INFO_3) }.doReturn(DISPLAY_NAME_3)
}
composeTestRule.setContent {
SimOnboardingSelectSimImpl(nextAction, cancelAction, mockSimOnboardingService)
}
composeTestRule.onNodeWithText(DISPLAY_NAME_1).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_1).assertIsDisplayed()
composeTestRule.onNodeWithText(DISPLAY_NAME_2).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_2).assertIsDisplayed()
composeTestRule.onNodeWithText(DISPLAY_NAME_3).assertIsDisplayed()
composeTestRule.onNodeWithText(NUMBER_3).assertIsDisplayed()
}
private companion object {
const val SUB_ID_1 = 1
const val SUB_ID_2 = 2
const val SUB_ID_3 = 3
const val DISPLAY_NAME_1 = "Sub 1"
const val DISPLAY_NAME_2 = "Sub 2"
const val DISPLAY_NAME_3 = "Sub 3"
const val NUMBER_1 = "000000001"
const val NUMBER_2 = "000000002"
const val NUMBER_3 = "000000003"
const val PRIMARY_SIM_ASK_EVERY_TIME = -1
val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setDisplayName(DISPLAY_NAME_1)
setNumber(NUMBER_1)
}.build()
val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
setDisplayName(DISPLAY_NAME_2)
setNumber(NUMBER_2)
}.build()
val SUB_INFO_3: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_3)
setDisplayName(DISPLAY_NAME_3)
setNumber(NUMBER_3)
}.build()
}
}