diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b64fc45487..4230b6a37a7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4697,14 +4697,6 @@
-
-
-
-
-
-
-
diff --git a/res/drawable/ic_audio_play_sample.xml b/res/drawable/ic_audio_play_sample.xml
new file mode 100644
index 00000000000..3666c22ce96
--- /dev/null
+++ b/res/drawable/ic_audio_play_sample.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/res/values/config.xml b/res/values/config.xml
index 6f784dd7705..f3e2a7ac042 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -793,4 +793,8 @@
false
+
+
+
+
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d5e08bbc7e4..9ffa2b27798 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -31,6 +31,13 @@
android:title="@string/calls_and_alarms_device_title"
settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" />
+
+
{
+ if (mRingtone == null) {
+ Log.d(TAG, "Skip onClick due to ringtone is null");
+ return true;
+ }
+ try {
+ mRingtone.setAudioAttributes(
+ new AudioAttributes.Builder(mRingtone.getAudioAttributes())
+ .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
+ .addTag("VX_AOSP_SAMPLESOUND")
+ .build());
+ if (!mRingtone.isPlaying()) {
+ mRingtone.play();
+ }
+ } catch (Throwable e) {
+ Log.w(TAG, "Fail to play sample, error = " + e);
+ }
+ return true;
+ });
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ super.onStop(owner);
+ if (mRingtone != null && mRingtone.isPlaying()) {
+ mRingtone.stop();
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ private Uri getMediaVolumeUri() {
+ return Uri.parse(
+ ContentResolver.SCHEME_ANDROID_RESOURCE
+ + "://"
+ + mContext.getPackageName()
+ + "/"
+ + R.raw.media_volume);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index ffb0b884fda..678f9524a37 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -35,21 +35,27 @@ import com.google.common.base.Strings;
*/
class AudioStreamPreference extends TwoTargetPreference {
private boolean mIsConnected = false;
+ private AudioStream mAudioStream;
/**
* Update preference UI based on connection status
*
- * @param isConnected Is this streams connected
+ * @param isConnected Is this stream connected
+ * @param summary Summary text
+ * @param onPreferenceClickListener Click listener for the preference
*/
void setIsConnected(
- boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
+ boolean isConnected,
+ String summary,
+ @Nullable OnPreferenceClickListener onPreferenceClickListener) {
if (mIsConnected == isConnected
+ && getSummary() == summary
&& getOnPreferenceClickListener() == onPreferenceClickListener) {
// Nothing to update.
return;
}
mIsConnected = isConnected;
- setSummary(isConnected ? "Listening now" : "");
+ setSummary(summary);
setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
@@ -60,6 +66,14 @@ class AudioStreamPreference extends TwoTargetPreference {
setIcon(R.drawable.ic_bt_audio_sharing);
}
+ void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mAudioStream.setState(state);
+ }
+
+ AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
+ return mAudioStream.getState();
+ }
+
@Override
protected boolean shouldHideSecondTarget() {
return mIsConnected;
@@ -71,19 +85,31 @@ class AudioStreamPreference extends TwoTargetPreference {
}
static AudioStreamPreference fromMetadata(
- Context context, BluetoothLeBroadcastMetadata source) {
+ Context context,
+ BluetoothLeBroadcastMetadata source,
+ AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
+ preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
return preference;
}
static AudioStreamPreference fromReceiveState(
- Context context, BluetoothLeBroadcastReceiveState state) {
+ Context context,
+ BluetoothLeBroadcastReceiveState receiveState,
+ AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
- preference.setTitle(getBroadcastName(state));
+ preference.setTitle(getBroadcastName(receiveState));
+ preference.setAudioStream(
+ new AudioStream(
+ receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
return preference;
}
+ private void setAudioStream(AudioStream audioStream) {
+ mAudioStream = audioStream;
+ }
+
private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
return source.getSubgroups().stream()
.map(s -> s.getContentMetadata().getProgramInfo())
@@ -99,4 +125,43 @@ class AudioStreamPreference extends TwoTargetPreference {
.findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId());
}
+
+ private static final class AudioStream {
+ private int mSourceId;
+ private int mBroadcastId;
+ private AudioStreamsProgressCategoryController.AudioStreamState mState;
+
+ private AudioStream(
+ int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mBroadcastId = broadcastId;
+ mState = state;
+ }
+
+ private AudioStream(
+ int sourceId,
+ int broadcastId,
+ AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mSourceId = sourceId;
+ mBroadcastId = broadcastId;
+ mState = state;
+ }
+
+ // TODO(chelseahao): use this to handleSourceRemoved
+ private int getSourceId() {
+ return mSourceId;
+ }
+
+ // TODO(chelseahao): use this to handleSourceRemoved
+ private int getBroadcastId() {
+ return mBroadcastId;
+ }
+
+ private AudioStreamsProgressCategoryController.AudioStreamState getState() {
+ return mState;
+ }
+
+ private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+ mState = state;
+ }
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index a418415d5f2..b0af7ddd78a 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -34,7 +34,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
public class AudioStreamsDashboardFragment extends DashboardFragment {
private static final String TAG = "AudioStreamsDashboardFrag";
private static final boolean DEBUG = BluetoothUtils.D;
- private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
+ private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
public AudioStreamsDashboardFragment() {
super();
@@ -69,8 +69,8 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
- mAudioStreamsScanQrCodeController.setFragment(this);
+ use(AudioStreamsScanQrCodeController.class).setFragment(this);
+ mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
}
@Override
@@ -103,11 +103,13 @@ public class AudioStreamsDashboardFragment extends DashboardFragment {
if (DEBUG) {
Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
}
- if (mAudioStreamsScanQrCodeController == null) {
- Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
+ if (mAudioStreamsProgressCategoryController == null) {
+ Log.w(
+ TAG,
+ "onActivityResult() AudioStreamsProgressCategoryController is null!");
return;
}
- mAudioStreamsScanQrCodeController.addSource(source);
+ mAudioStreamsProgressCategoryController.setSourceFromQrCode(source);
}
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 198e8e5f335..2c6eedbfbf7 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -109,13 +109,14 @@ class AudioStreamsHelper {
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
- List getAllSources() {
+ List getAllConnectedSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getActiveSinksOnAssistant(mBluetoothManager).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+ .filter(this::isConnected)
.toList();
}
@@ -124,7 +125,7 @@ class AudioStreamsHelper {
return mLeBroadcastAssistant;
}
- static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
+ boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 3c005b294b5..ab380c811ba 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -25,6 +25,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
+import android.os.CountDownTimer;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -71,6 +72,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
};
+ enum AudioStreamState {
+ // When mTimedSourceFromQrCode is present and this source has not been synced.
+ WAIT_FOR_SYNC,
+ // When source has been synced but not added to any sink.
+ SYNCED,
+ // When addSource is called for this source and waiting for response.
+ WAIT_FOR_SOURCE_ADD,
+ // Source is added to active sink.
+ SOURCE_ADDED,
+ }
+
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
@@ -78,6 +90,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
+ private TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
@@ -122,6 +135,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mExecutor.execute(this::stopScanning);
}
+ void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
+ mTimedSourceFromQrCode =
+ new TimedSourceFromQrCode(
+ mContext, source, () -> handleSourceLost(source.getBroadcastId()));
+ }
+
void setScanning(boolean isScanning) {
ThreadUtils.postOnMainThread(
() -> {
@@ -140,24 +159,90 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
if (source.isEncrypted()) {
ThreadUtils.postOnMainThread(
- () -> launchPasswordDialog(source, preference));
+ () ->
+ launchPasswordDialog(
+ source, (AudioStreamPreference) preference));
} else {
mAudioStreamsHelper.addSource(source);
+ ((AudioStreamPreference) preference)
+ .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ (AudioStreamPreference) preference,
+ AudioStreamState.WAIT_FOR_SOURCE_ADD,
+ null);
}
return true;
};
- mBroadcastIdToPreferenceMap.computeIfAbsent(
- source.getBroadcastId(),
- k -> {
- var preference = AudioStreamPreference.fromMetadata(mContext, source);
- ThreadUtils.postOnMainThread(
- () -> {
- preference.setIsConnected(false, addSourceOrShowDialog);
- if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(preference);
- }
- });
- return preference;
+
+ var broadcastIdFound = source.getBroadcastId();
+ mBroadcastIdToPreferenceMap.compute(
+ broadcastIdFound,
+ (k, v) -> {
+ if (v == null) {
+ return addNewPreference(
+ source, AudioStreamState.SYNCED, addSourceOrShowDialog);
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ var pendingSource = mTimedSourceFromQrCode.get();
+ if (pendingSource == null) {
+ Log.w(
+ TAG,
+ "handleSourceFound(): unexpected state with null pendingSource:"
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdFound);
+ v.setAudioStreamState(AudioStreamState.SYNCED);
+ return v;
+ }
+ mAudioStreamsHelper.addSource(pendingSource);
+ mTimedSourceFromQrCode.consumed();
+ v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ } else {
+ if (fromState != AudioStreamState.SOURCE_ADDED) {
+ Log.w(
+ TAG,
+ "handleSourceFound(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdFound);
+ }
+ }
+ return v;
+ });
+ }
+
+ private void handleSourceFromQrCodeIfExists() {
+ if (mTimedSourceFromQrCode == null || mTimedSourceFromQrCode.get() == null) {
+ return;
+ }
+ var metadataFromQrCode = mTimedSourceFromQrCode.get();
+ mBroadcastIdToPreferenceMap.compute(
+ metadataFromQrCode.getBroadcastId(),
+ (k, v) -> {
+ if (v == null) {
+ mTimedSourceFromQrCode.waitForConsume();
+ return addNewPreference(
+ metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.SYNCED) {
+ mAudioStreamsHelper.addSource(metadataFromQrCode);
+ mTimedSourceFromQrCode.consumed();
+ v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ } else {
+ Log.w(
+ TAG,
+ "handleSourceFromQrCode(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + metadataFromQrCode.getBroadcastId());
+ }
+ return v;
});
}
@@ -174,32 +259,54 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId);
}
- void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
- if (!AudioStreamsHelper.isConnected(state)) {
+ void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
+ if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
+ var sourceAddedState = AudioStreamState.SOURCE_ADDED;
+ var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
- state.getBroadcastId(),
+ broadcastIdConnected,
(k, v) -> {
- // True if this source has been added either by scanning, or it's currently
- // connected to another active sink.
- boolean existed = v != null;
- AudioStreamPreference preference =
- existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
-
- ThreadUtils.postOnMainThread(
- () -> {
- preference.setIsConnected(
- true, p -> launchDetailFragment(state.getBroadcastId()));
- if (mCategoryPreference != null && !existed) {
- mCategoryPreference.addPreference(preference);
- }
- });
-
- return preference;
+ if (v == null) {
+ return addNewPreference(
+ receiveState,
+ sourceAddedState,
+ p -> launchDetailFragment(broadcastIdConnected));
+ }
+ var fromState = v.getAudioStreamState();
+ if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
+ || fromState == AudioStreamState.SYNCED
+ || fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed();
+ }
+ } else {
+ if (fromState != AudioStreamState.SOURCE_ADDED) {
+ Log.w(
+ TAG,
+ "handleSourceConnected(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdConnected);
+ }
+ }
+ v.setAudioStreamState(sourceAddedState);
+ updatePreferenceConnectionState(
+ v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
+ return v;
});
}
+ private static String getPreferenceSummary(AudioStreamState state) {
+ return switch (state) {
+ case WAIT_FOR_SYNC -> "Scanning...";
+ case WAIT_FOR_SOURCE_ADD -> "Connecting...";
+ case SOURCE_ADDED -> "Listening now";
+ default -> "";
+ };
+ }
+
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -235,13 +342,15 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
- // Display currently connected streams
+ // Handle QR code scan and display currently connected streams
var unused =
ThreadUtils.postOnBackgroundThread(
- () ->
- mAudioStreamsHelper
- .getAllSources()
- .forEach(this::handleSourceConnected));
+ () -> {
+ handleSourceFromQrCodeIfExists();
+ mAudioStreamsHelper
+ .getAllConnectedSources()
+ .forEach(this::handleSourceConnected);
+ });
}
private void stopScanning() {
@@ -256,6 +365,43 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mLeBroadcastAssistant.stopSearchingForSources();
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed();
+ }
+ }
+
+ private AudioStreamPreference addNewPreference(
+ BluetoothLeBroadcastReceiveState receiveState,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
+ updatePreferenceConnectionState(preference, state, onClickListener);
+ return preference;
+ }
+
+ private AudioStreamPreference addNewPreference(
+ BluetoothLeBroadcastMetadata metadata,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
+ updatePreferenceConnectionState(preference, state, onClickListener);
+ return preference;
+ }
+
+ private void updatePreferenceConnectionState(
+ AudioStreamPreference preference,
+ AudioStreamState state,
+ Preference.OnPreferenceClickListener onClickListener) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ preference.setIsConnected(
+ state == AudioStreamState.SOURCE_ADDED,
+ getPreferenceSummary(state),
+ onClickListener);
+ if (mCategoryPreference != null) {
+ mCategoryPreference.addPreference(preference);
+ }
+ });
}
private boolean launchDetailFragment(int broadcastId) {
@@ -282,7 +428,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
return true;
}
- private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
+ private void launchPasswordDialog(
+ BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
@@ -307,8 +454,49 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
.build());
+ preference.setAudioStreamState(
+ AudioStreamState.WAIT_FOR_SOURCE_ADD);
+ updatePreferenceConnectionState(
+ preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
})
.create();
alertDialog.show();
}
+
+ private static class TimedSourceFromQrCode {
+ private static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
+ private final CountDownTimer mTimer;
+ private BluetoothLeBroadcastMetadata mSourceFromQrCode;
+
+ private TimedSourceFromQrCode(
+ Context context,
+ BluetoothLeBroadcastMetadata sourceFromQrCode,
+ Runnable timeoutAction) {
+ mSourceFromQrCode = sourceFromQrCode;
+ mTimer =
+ new CountDownTimer(WAIT_FOR_SYNC_TIMEOUT_MILLIS, 1000) {
+ @Override
+ public void onTick(long millisUntilFinished) {}
+
+ @Override
+ public void onFinish() {
+ timeoutAction.run();
+ AudioSharingUtils.toastMessage(context, "Audio steam isn't available");
+ }
+ };
+ }
+
+ private void waitForConsume() {
+ mTimer.start();
+ }
+
+ private void consumed() {
+ mTimer.cancel();
+ mSourceFromQrCode = null;
+ }
+
+ private BluetoothLeBroadcastMetadata get() {
+ return mSourceFromQrCode;
+ }
+ }
}
diff --git a/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java
index d0b57fd347a..6a65dc07c88 100644
--- a/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java
@@ -80,7 +80,8 @@ public class BatteryHeaderPreferenceController extends BasePreferenceController
return mContext.getString(
com.android.settingslib.R.string.battery_info_status_not_charging);
} else if (BatteryUtils.isBatteryDefenderOn(info)) {
- return null;
+ return mContext.getString(
+ com.android.settingslib.R.string.battery_info_status_charging_on_hold);
} else if (info.remainingLabel == null
|| info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
// Present status only if no remaining time or status anomalous
diff --git a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
index 0f54f3e7d03..365a2cacb89 100644
--- a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
@@ -153,6 +153,11 @@ public class TopLevelBatteryPreferenceController extends BasePreferenceControlle
return mContext.getString(
com.android.settingslib.R.string.battery_info_status_not_charging);
}
+ if (BatteryUtils.isBatteryDefenderOn(info)) {
+ return mContext.getString(
+ com.android.settingslib.R.string.power_charging_on_hold_settings_home_page,
+ info.batteryPercentString);
+ }
if (info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
// Present status only if no remaining time or status anomalous
return info.statusLabel;
diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
index b1018ba34cd..a80987ddb17 100644
--- a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
+++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
@@ -49,6 +49,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import com.android.settingslib.utils.ThreadUtils;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -59,7 +60,7 @@ public class AnomalyDetectionJobService extends JobService {
private static final int ON = 1;
@VisibleForTesting static final int UID_NULL = -1;
@VisibleForTesting static final int STATSD_UID_FILED = 1;
- @VisibleForTesting static final long MAX_DELAY_MS = TimeUnit.MINUTES.toMillis(30);
+ @VisibleForTesting static final long MAX_DELAY_MS = Duration.ofDays(1).toMillis();
private final Object mLock = new Object();
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 52066a17a25..2600618caae 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -101,17 +101,19 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur
RegularScaffold(
title = if (apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
actions = {
- IconButton(onClick = {
- if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
- val valid = validateAndSaveApnData(
- apnDataInit,
- apnData,
- context,
- uriInit,
- networkTypeSelectedOptionsState
- )
- if (valid) navController.navigateBack()
- }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+ if (!apnData.customizedConfig.readOnlyApn) {
+ IconButton(onClick = {
+ if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
+ val valid = validateAndSaveApnData(
+ apnDataInit,
+ apnData,
+ context,
+ uriInit,
+ networkTypeSelectedOptionsState
+ )
+ if (valid) navController.navigateBack()
+ }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+ }
},
) {
Column {
@@ -212,7 +214,9 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur
emptyVal = stringResource(R.string.network_type_unspecified),
enabled = apnData.networkTypeEnabled
) {}
- if (!apnData.newApn) {
+ if (!apnData.newApn && !apnData.customizedConfig.readOnlyApn
+ && apnData.customizedConfig.isAddApnAllowed
+ ) {
Preference(
object : PreferenceModel {
override val title = stringResource(R.string.menu_delete)
diff --git a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
index 27b8c16e109..441c2498152 100644
--- a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
+++ b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
@@ -51,6 +51,7 @@ import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
public class ConvertToEsimPreferenceController extends TelephonyBasePreferenceController implements
@@ -111,7 +112,8 @@ public class ConvertToEsimPreferenceController extends TelephonyBasePreferenceCo
* To avoid showing users dialogs that can cause confusion,
* add conditions to allow conversion in the absence of active eSIM.
*/
- if (!mContext.getResources().getBoolean(R.bool.config_psim_conversion_menu_enabled)) {
+ if (!mContext.getResources().getBoolean(R.bool.config_psim_conversion_menu_enabled)
+ || !isPsimConversionSupport(subId)) {
return CONDITIONALLY_UNAVAILABLE;
}
if (findConversionSupportComponent()) {
@@ -238,4 +240,16 @@ public class ConvertToEsimPreferenceController extends TelephonyBasePreferenceCo
}
return true;
}
+
+ private boolean isPsimConversionSupport(int subId) {
+ SubscriptionManager subscriptionManager = mContext.getSystemService(
+ SubscriptionManager.class);
+ SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId);
+ if (subInfo == null) {
+ return false;
+ }
+ final int[] supportedCarriers = mContext.getResources().getIntArray(
+ R.array.config_psim_conversion_menu_enabled_carrier);
+ return Arrays.stream(supportedCarriers).anyMatch(id -> id == subInfo.getCarrierId());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
index a67e5d37e19..482f0d0ce02 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobServiceTest.java
@@ -71,6 +71,7 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ServiceController;
import org.robolectric.annotation.Config;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -133,7 +134,7 @@ public class AnomalyDetectionJobServiceTest {
JobInfo pendingJob = pendingJobs.get(0);
assertThat(pendingJob.getId()).isEqualTo(R.integer.job_anomaly_detection);
assertThat(pendingJob.getMaxExecutionDelayMillis())
- .isEqualTo(TimeUnit.MINUTES.toMillis(30));
+ .isEqualTo(Duration.ofDays(1).toMillis());
}
@Test
diff --git a/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
index f060588b498..b33a5643b6c 100644
--- a/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/widget/LinkifySummaryPreferenceTest.java
@@ -32,21 +32,22 @@ import android.view.View;
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
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;
-import org.robolectric.RuntimeEnvironment;
-@Ignore("b/313563183")
@RunWith(RobolectricTestRunner.class)
public class LinkifySummaryPreferenceTest {
- @Spy
+ @Rule
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
private PreferenceViewHolder mViewHolder;
@Mock
private TextView mSummaryTextView;
@@ -54,9 +55,7 @@ public class LinkifySummaryPreferenceTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- final Context context = RuntimeEnvironment.application;
+ final Context context = ApplicationProvider.getApplicationContext();
mPreference = new LinkifySummaryPreference(context, null /* attrs */);
final View view = spy(View.inflate(context, mPreference.getLayoutResource(),