Merge "[Reskin] Replace string for chargin on hold" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b64fc4..4230b6a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4697,14 +4697,6 @@
         <receiver android:name=".fuelgauge.batterytip.AnomalyDetectionReceiver"
                   android:exported="false" />
 
-        <receiver android:name=".fuelgauge.batterytip.AnomalyConfigReceiver"
-            android:exported="true">
-            <intent-filter>
-                <action android:name="android.app.action.STATSD_STARTED"/>
-                <action android:name="android.intent.action.BOOT_COMPLETED"/>
-            </intent-filter>
-        </receiver>
-
         <service android:name=".fuelgauge.batterytip.AnomalyCleanupJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" />
 
diff --git a/res/drawable/ic_audio_play_sample.xml b/res/drawable/ic_audio_play_sample.xml
new file mode 100644
index 0000000..3666c22
--- /dev/null
+++ b/res/drawable/ic_audio_play_sample.xml
@@ -0,0 +1,32 @@
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+  <path
+      android:pathData="M14,8C9.6,8 6,11.6 6,16H8C8,12.7 10.7,10 14,10V8Z"
+      android:fillColor="#4E4639"/>
+  <path
+      android:pathData="M14,6V4C7.4,4 2,9.4 2,16H4C4,10.5 8.5,6 14,6Z"
+      android:fillColor="#4E4639"/>
+  <path
+      android:pathData="M16,4V12.6C15.4,12.3 14.7,12 14,12C11.8,12 10,13.8 10,16C10,18.2 11.8,20 14,20C16.2,20 18,18.2 18,16V7H22V4H16ZM14,18C12.9,18 12,17.1 12,16C12,14.9 12.9,14 14,14C15.1,14 16,14.9 16,16C16,17.1 15.1,18 14,18Z"
+      android:fillColor="#4E4639"/>
+</vector>
diff --git a/res/values/config.xml b/res/values/config.xml
index 6f784dd..f3e2a7a 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -793,4 +793,8 @@
 
     <!-- Whether to display pSIM conversion menu in Settings.-->
     <bool name="config_psim_conversion_menu_enabled">false</bool>
+
+    <!-- Array of carrier id to allow the pSIM conversion-->
+    <integer-array name="config_psim_conversion_menu_enabled_carrier" translatable="false">
+    </integer-array>
 </resources>
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d5e08bb..9ffa2b2 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" />
 
+    <Preference
+        android:icon="@drawable/ic_audio_play_sample"
+        android:key="audio_sharing_play_sound"
+        android:summary="Everyone listening should hear it"
+        android:title="Play a test sound"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPlaySoundPreferenceController" />
+
     <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
         android:key="audio_sharing_stream_name"
         android:summary="********"
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
index 52a8f18..9105297 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -34,6 +34,7 @@
     private AudioSharingSwitchBarController mSwitchBarController;
     private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
     private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
+    private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
     private AudioSharingNamePreferenceController mAudioSharingNamePreferenceController;
     private AudioStreamsCategoryController mAudioStreamsCategoryController;
 
@@ -74,6 +75,8 @@
         mAudioSharingDeviceVolumeGroupController.init(this);
         mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
         mCallsAndAlarmsPreferenceController.init(this);
+        mAudioSharingPlaySoundPreferenceController =
+                use(AudioSharingPlaySoundPreferenceController.class);
         mAudioSharingNamePreferenceController = use(AudioSharingNamePreferenceController.class);
         mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
     }
@@ -100,6 +103,7 @@
     private void updateVisibilityForAttachedPreferences() {
         mAudioSharingDeviceVolumeGroupController.updateVisibility();
         mCallsAndAlarmsPreferenceController.updateVisibility();
+        mAudioSharingPlaySoundPreferenceController.updateVisibility();
         mAudioSharingNamePreferenceController.updateVisibility();
         mAudioStreamsCategoryController.updateVisibility();
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
new file mode 100644
index 0000000..6722219
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
@@ -0,0 +1,98 @@
+/*
+ * 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.connecteddevice.audiosharing;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+
+public class AudioSharingPlaySoundPreferenceController
+        extends AudioSharingBasePreferenceController {
+
+    private static final String TAG = "AudioSharingPlaySoundPreferenceController";
+
+    private static final String PREF_KEY = "audio_sharing_play_sound";
+
+    private final Ringtone mRingtone;
+
+    public AudioSharingPlaySoundPreferenceController(Context context) {
+        super(context, PREF_KEY);
+        mRingtone = RingtoneManager.getRingtone(context, getMediaVolumeUri());
+        if (mRingtone != null) {
+            mRingtone.setStreamType(AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference.setVisible(mRingtone != null);
+        mPreference.setOnPreferenceClickListener(
+                (v) -> {
+                    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 ffb0b88..678f952 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 @@
  */
 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 @@
         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 @@
     }
 
     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 @@
                 .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 a418415..b0af7dd 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 @@
 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 @@
     @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 @@
                 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 198e8e5..2c6eedb 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 @@
     }
 
     /** Retrieves a list of all LE broadcast receive states from active sinks. */
-    List<BluetoothLeBroadcastReceiveState> getAllSources() {
+    List<BluetoothLeBroadcastReceiveState> 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 @@
         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 3c005b2..ab380c8 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.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 @@
                 }
             };
 
+    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 @@
     private final @Nullable LocalBluetoothManager mBluetoothManager;
     private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
             new ConcurrentHashMap<>();
+    private TimedSourceFromQrCode mTimedSourceFromQrCode;
     private AudioStreamsProgressCategoryPreference mCategoryPreference;
 
     public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
@@ -122,6 +135,12 @@
         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 @@
                     }
                     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 @@
         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 @@
         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 @@
             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 @@
         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 @@
                                                     .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/batterytip/AnomalyDetectionJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java
index b1018ba..a80987d 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.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 @@
     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 52066a1..2600618 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -101,17 +101,19 @@
     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 @@
                 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 27b8c16..441c249 100644
--- a/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
+++ b/src/com/android/settings/network/telephony/ConvertToEsimPreferenceController.java
@@ -51,6 +51,7 @@
 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 @@
          * 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 @@
         }
         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 a67e5d3..482f0d0 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.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 @@
         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 f060588..b33a564 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.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 @@
 
     @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(),