Merge "Avoid empty line for storage summary" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2ba87d7..02dd9cd 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5036,6 +5036,12 @@
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
 
+        <activity
+            android:name="com.android.settings.network.WepNetworkDialogActivity"
+            android:exported="false"
+            android:theme="@style/Theme.SpaLib.Dialog">
+        </activity>
+
         <!-- This is the longest AndroidManifest.xml ever. -->
     </application>
 </manifest>
diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml
new file mode 100644
index 0000000..afc5055
--- /dev/null
+++ b/res/xml/bluetooth_audio_streams_dialog.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/dialog_bg"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/broadcast_dialog_margin"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/dialog_icon"
+                android:layout_width="36dp"
+                android:layout_height="36dp"
+                android:layout_marginTop="@dimen/broadcast_dialog_icon_margin_top"
+                android:layout_marginBottom="@dimen/broadcast_dialog_title_img_margin_top"
+                android:layout_gravity="center"
+                android:src="@drawable/ic_bt_audio_sharing"/>
+
+            <TextView
+                style="@style/BroadcastDialogTitleStyle"
+                android:id="@+id/dialog_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:layout_gravity="center"/>
+
+            <TextView
+                style="@style/BroadcastDialogBodyStyle"
+                android:id="@+id/dialog_subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:layout_gravity="center"
+                android:visibility="gone"/>
+
+            <TextView
+                style="@style/BroadcastDialogBodyStyle"
+                android:id="@+id/dialog_subtitle_2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:layout_gravity="center"
+                android:visibility="gone"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/broadcast_dialog_margin"
+            android:orientation="horizontal">
+            <Button
+                android:id="@+id/left_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="16dp"
+                android:layout_weight="1"
+                android:visibility="gone"/>
+            <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"/>
+        </LinearLayout>
+
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java
index fb79ece..78f5b4c 100644
--- a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java
+++ b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java
@@ -149,6 +149,7 @@
         for (BluetoothGatt gatt: mConnectingGattList) {
             gatt.disconnect();
         }
+        mConnectingGattList.clear();
         mLocalManager.setForegroundActivity(null);
         mLocalManager.getEventManager().unregisterCallback(this);
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index b0af7dd..b5c71b6 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -71,6 +71,7 @@
         super.onAttach(context);
         use(AudioStreamsScanQrCodeController.class).setFragment(this);
         mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
+        mAudioStreamsProgressCategoryController.setFragment(this);
     }
 
     @Override
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java
new file mode 100644
index 0000000..c7d7f16
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+import com.google.common.base.Strings;
+
+import java.util.function.Consumer;
+
+public class AudioStreamsDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioStreamsDialogFragment";
+    private final DialogBuilder mDialogBuilder;
+
+    AudioStreamsDialogFragment(DialogBuilder dialogBuilder) {
+        mDialogBuilder = dialogBuilder;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO(chelseahao): update metrics id
+        return 0;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return mDialogBuilder.build();
+    }
+
+    static void show(Fragment host, DialogBuilder dialogBuilder) {
+        FragmentManager manager = host.getChildFragmentManager();
+        (new AudioStreamsDialogFragment(dialogBuilder)).show(manager, TAG);
+    }
+
+    static class DialogBuilder {
+        private final Context mContext;
+        private final AlertDialog.Builder mBuilder;
+        private String mTitle;
+        private String mSubTitle1;
+        private String mSubTitle2;
+        private String mLeftButtonText;
+        private String mRightButtonText;
+        private Consumer<AlertDialog> mLeftButtonOnClickListener;
+        private Consumer<AlertDialog> mRightButtonOnClickListener;
+
+        DialogBuilder(Context context) {
+            mContext = context;
+            mBuilder = new AlertDialog.Builder(context);
+        }
+
+        DialogBuilder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        DialogBuilder setSubTitle1(String subTitle1) {
+            mSubTitle1 = subTitle1;
+            return this;
+        }
+
+        DialogBuilder setSubTitle2(String subTitle2) {
+            mSubTitle2 = subTitle2;
+            return this;
+        }
+
+        DialogBuilder setLeftButtonText(String text) {
+            mLeftButtonText = text;
+            return this;
+        }
+
+        DialogBuilder setLeftButtonOnClickListener(Consumer<AlertDialog> listener) {
+            mLeftButtonOnClickListener = listener;
+            return this;
+        }
+
+        DialogBuilder setRightButtonText(String text) {
+            mRightButtonText = text;
+            return this;
+        }
+
+        DialogBuilder setRightButtonOnClickListener(Consumer<AlertDialog> listener) {
+            mRightButtonOnClickListener = listener;
+            return this;
+        }
+
+        AlertDialog build() {
+            View rootView =
+                    LayoutInflater.from(mContext)
+                            .inflate(R.xml.bluetooth_audio_streams_dialog, /* parent= */ null);
+
+            AlertDialog dialog = mBuilder.setView(rootView).setCancelable(false).create();
+            dialog.setCanceledOnTouchOutside(false);
+
+            TextView title = rootView.requireViewById(R.id.dialog_title);
+            title.setText(mTitle);
+
+            if (!Strings.isNullOrEmpty(mSubTitle1)) {
+                TextView subTitle1 = rootView.requireViewById(R.id.dialog_subtitle);
+                subTitle1.setText(mSubTitle1);
+                subTitle1.setVisibility(View.VISIBLE);
+            }
+            if (!Strings.isNullOrEmpty(mSubTitle2)) {
+                TextView subTitle2 = rootView.requireViewById(R.id.dialog_subtitle_2);
+                subTitle2.setText(mSubTitle2);
+                subTitle2.setVisibility(View.VISIBLE);
+            }
+            if (!Strings.isNullOrEmpty(mLeftButtonText)) {
+                Button leftButton = rootView.requireViewById(R.id.left_button);
+                leftButton.setText(mLeftButtonText);
+                leftButton.setVisibility(View.VISIBLE);
+                leftButton.setOnClickListener(unused -> mLeftButtonOnClickListener.accept(dialog));
+            }
+            if (!Strings.isNullOrEmpty(mRightButtonText)) {
+                Button rightButton = rootView.requireViewById(R.id.right_button);
+                rightButton.setText(mRightButtonText);
+                rightButton.setVisibility(View.VISIBLE);
+                rightButton.setOnClickListener(
+                        unused -> mRightButtonOnClickListener.accept(dialog));
+            }
+
+            return dialog;
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index ab380c8..cb9975d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -24,8 +24,10 @@
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.CountDownTimer;
+import android.provider.Settings;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -92,6 +94,7 @@
             new ConcurrentHashMap<>();
     private TimedSourceFromQrCode mTimedSourceFromQrCode;
     private AudioStreamsProgressCategoryPreference mCategoryPreference;
+    private AudioStreamsDashboardFragment mFragment;
 
     public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
         super(context, preferenceKey);
@@ -135,10 +138,13 @@
         mExecutor.execute(this::stopScanning);
     }
 
+    void setFragment(AudioStreamsDashboardFragment fragment) {
+        mFragment = fragment;
+    }
+
     void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
         mTimedSourceFromQrCode =
-                new TimedSourceFromQrCode(
-                        mContext, source, () -> handleSourceLost(source.getBroadcastId()));
+                new TimedSourceFromQrCode(source, () -> handleSourceLost(source.getBroadcastId()));
     }
 
     void setScanning(boolean isScanning) {
@@ -324,6 +330,8 @@
             startScanning();
         } else {
             stopScanning();
+            ThreadUtils.postOnMainThread(
+                    () -> AudioStreamsDialogFragment.show(mFragment, getNoLeDeviceDialog()));
         }
     }
 
@@ -463,15 +471,41 @@
         alertDialog.show();
     }
 
-    private static class TimedSourceFromQrCode {
+    private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
+        return new AudioStreamsDialogFragment.DialogBuilder(mContext)
+                .setTitle("Connect compatible headphones")
+                .setSubTitle1(
+                        "To listen to an audio stream, first connect headphones that support LE"
+                                + " Audio to this device. Learn more")
+                .setLeftButtonText("Close")
+                .setLeftButtonOnClickListener(AlertDialog::dismiss)
+                .setRightButtonText("Connect a device")
+                .setRightButtonOnClickListener(
+                        unused ->
+                                mContext.startActivity(
+                                        new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)));
+    }
+
+    private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
+            String broadcastName) {
+        return new AudioStreamsDialogFragment.DialogBuilder(mContext)
+                .setTitle("Audio stream isn't available")
+                .setSubTitle1(broadcastName)
+                .setSubTitle2("This audio stream isn't playing anything right now")
+                .setLeftButtonText("Close")
+                .setLeftButtonOnClickListener(AlertDialog::dismiss)
+                .setRightButtonText("Retry")
+                // TODO(chelseahao): Add retry action
+                .setRightButtonOnClickListener(AlertDialog::dismiss);
+    }
+
+    private 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) {
+                BluetoothLeBroadcastMetadata sourceFromQrCode, Runnable timeoutAction) {
             mSourceFromQrCode = sourceFromQrCode;
             mTimer =
                     new CountDownTimer(WAIT_FOR_SYNC_TIMEOUT_MILLIS, 1000) {
@@ -481,7 +515,12 @@
                         @Override
                         public void onFinish() {
                             timeoutAction.run();
-                            AudioSharingUtils.toastMessage(context, "Audio steam isn't available");
+                            ThreadUtils.postOnMainThread(
+                                    () ->
+                                            AudioStreamsDialogFragment.show(
+                                                    mFragment,
+                                                    getBroadcastUnavailableDialog(
+                                                            sourceFromQrCode.getBroadcastName())));
                         }
                     };
         }
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index f14c32c..0da1034 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -89,6 +89,7 @@
 import com.android.settingslib.widget.LayoutPreference;
 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
 import com.android.settingslib.wifi.WifiSavedConfigUtils;
+import com.android.wifi.flags.Flags;
 import com.android.wifitrackerlib.WifiEntry;
 import com.android.wifitrackerlib.WifiEntry.ConnectCallback;
 import com.android.wifitrackerlib.WifiPickerTracker;
@@ -1257,8 +1258,19 @@
 
         // If it's an unsaved secure WifiEntry, it will callback
         // ConnectCallback#onConnectResult with ConnectCallback#CONNECT_STATUS_FAILURE_NO_CONFIG
-        wifiEntry.connect(new WifiEntryConnectCallback(wifiEntry, editIfNoConfig,
-                fullScreenEdit));
+        WifiEntryConnectCallback callback =
+                new WifiEntryConnectCallback(wifiEntry, editIfNoConfig, fullScreenEdit);
+
+        if (Flags.wepUsage() && wifiEntry.getSecurityTypes().contains(WifiEntry.SECURITY_WEP)) {
+            WepNetworkDialogActivity.checkWepAllowed(
+                    getContext(), getViewLifecycleOwner(), wifiEntry.getSsid(), () -> {
+                        wifiEntry.connect(callback);
+                        return null;
+                    });
+            return;
+        }
+
+        wifiEntry.connect(callback);
     }
 
     private class WifiConnectActionListener implements WifiManager.ActionListener {
diff --git a/src/com/android/settings/network/WepNetworkDialogActivity.kt b/src/com/android/settings/network/WepNetworkDialogActivity.kt
new file mode 100644
index 0000000..2fa8784
--- /dev/null
+++ b/src/com/android/settings/network/WepNetworkDialogActivity.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.Intent
+import android.net.wifi.WifiManager
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextAlign
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.android.settings.R
+import com.android.settings.core.SubSettingLauncher
+import com.android.settings.wifi.ConfigureWifiSettings
+import com.android.settingslib.spa.SpaBaseDialogActivity
+import com.android.settingslib.spa.widget.dialog.AlertDialogButton
+import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon
+import kotlin.coroutines.resume
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+class WepNetworkDialogActivity : SpaBaseDialogActivity() {
+    @Composable
+    override fun Content() {
+        val context = LocalContext.current
+        val wifiManager = context.getSystemService(WifiManager::class.java)
+        SettingsAlertDialogWithIcon(
+            onDismissRequest = { finish() },
+            confirmButton = AlertDialogButton(
+                getString(R.string.wifi_settings_ssid_block_button_close)
+            ) { finish() },
+            dismissButton = AlertDialogButton(
+                getString(R.string.wifi_settings_wep_networks_button_allow)
+            ) {
+                SubSettingLauncher(context)
+                    .setTitleText(context.getText(R.string.network_and_internet_preferences_title))
+                    .setSourceMetricsCategory(SettingsEnums.CONFIGURE_WIFI)
+                    .setDestination(ConfigureWifiSettings::class.java.getName())
+                    .launch()
+                finish()
+            },
+            title = String.format(
+                getString(R.string.wifi_settings_wep_networks_blocked_title),
+                intent.getStringExtra(SSID) ?: SSID
+            ),
+            text = {
+                Text(
+                    if (wifiManager?.isWepSupported == false)
+                        getString(R.string.wifi_settings_wep_networks_summary_toggle_off)
+                    else getString(R.string.wifi_settings_wep_networks_summary_blocked_by_carrier),
+                    modifier = Modifier.fillMaxWidth(),
+                    textAlign = TextAlign.Center
+                )
+            })
+    }
+
+    companion object {
+        @JvmStatic
+        fun checkWepAllowed(
+            context: Context,
+            lifecycleOwner: LifecycleOwner,
+            ssid: String,
+            onAllowed: () -> Unit,
+        ) {
+            lifecycleOwner.lifecycleScope.launch {
+                val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch
+                if (wifiManager.queryWepAllowed()) {
+                    onAllowed()
+                } else {
+                    val intent = Intent(context, WepNetworkDialogActivity::class.java).apply {
+                        putExtra(SSID, ssid)
+                    }
+                    context.startActivity(intent)
+                }
+            }
+        }
+
+        private suspend fun WifiManager.queryWepAllowed(): Boolean =
+            withContext(Dispatchers.Default) {
+                suspendCancellableCoroutine { continuation ->
+                    queryWepAllowed(Dispatchers.Default.asExecutor()) {
+                        continuation.resume(it)
+                    }
+                }
+            }
+
+        const val SSID = "ssid"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
index e4fb1ea..38a8499 100644
--- a/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
@@ -104,7 +104,7 @@
             userHandle
         )
         try {
-            packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender, 0)
+            packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender)
         } catch (e: Exception) {
             Log.e(LOG_TAG, "Request archive failed", e)
             Toast.makeText(
diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
index a7f9714..aafe493 100644
--- a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
@@ -56,9 +56,12 @@
     override val appOp = AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
     override val permission = Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO
     override val setModeByUid = true
-
+    private var receiveDetectionTrainingDataOpController:AppOpsController? = null
     override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
         super.setAllowed(record, newAllowed)
+        if (!newAllowed && receiveDetectionTrainingDataOpController != null) {
+            receiveDetectionTrainingDataOpController!!.setAllowed(false)
+        }
         logPermissionChange(newAllowed)
     }
 
@@ -79,20 +82,21 @@
         isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean?
     ): ReceiveDetectionTrainingDataOpSwitchModel {
         val context = LocalContext.current
-        val ReceiveDetectionTrainingDataOpController = remember {
+        receiveDetectionTrainingDataOpController = remember {
             AppOpsController(
                 context = context,
                 app = record.app,
                 op = AppOpsManager.OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
             )
         }
-        val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, ReceiveDetectionTrainingDataOpController)
+        val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, receiveDetectionTrainingDataOpController!!)
+
         return remember(record) {
             ReceiveDetectionTrainingDataOpSwitchModel(
                 context,
                 record,
                 isReceiveSandBoxTriggerAudioOpAllowed,
-                ReceiveDetectionTrainingDataOpController,
+                receiveDetectionTrainingDataOpController!!,
                 isReceiveDetectionTrainingDataOpAllowed,
             )
         }.also { model -> LaunchedEffect(model, Dispatchers.Default) { model.initState() } }
diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java
index e62e548..6d81179 100644
--- a/src/com/android/settings/system/FactoryResetPreferenceController.java
+++ b/src/com/android/settings/system/FactoryResetPreferenceController.java
@@ -28,6 +28,7 @@
 import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.preference.Preference;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.settings.Settings;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.factory_reset.Flags;
@@ -36,11 +37,14 @@
 
     private static final String TAG = "FactoryResetPreference";
 
-    private static final String ACTION_PREPARE_FACTORY_RESET =
+    @VisibleForTesting
+    static final String ACTION_PREPARE_FACTORY_RESET =
             "com.android.settings.ACTION_PREPARE_FACTORY_RESET";
 
     private final UserManager mUm;
-    private ActivityResultLauncher<Intent> mFactoryResetPreparationLauncher;
+
+    @VisibleForTesting
+    ActivityResultLauncher<Intent> mFactoryResetPreparationLauncher;
 
     public FactoryResetPreferenceController(Context context, String preferenceKey) {
         super(context, preferenceKey);
diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
index 321fcf5..01d0df9 100644
--- a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java
@@ -17,75 +17,125 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 
-import com.android.settings.testutils.shadow.ShadowUserManager;
-import com.android.settings.testutils.shadow.ShadowUtils;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.preference.Preference;
+
+import com.google.common.collect.ImmutableList;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
 
 @RunWith(RobolectricTestRunner.class)
-@Config(shadows = ShadowUserManager.class)
 public class FactoryResetPreferenceControllerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final String FACTORY_RESET_KEY = "factory_reset";
+    private static final String FACTORY_RESET_APP_PACKAGE = "com.frw_app";
 
-    private ShadowUserManager mShadowUserManager;
+    @Mock private ActivityResultLauncher<Intent> mFactoryResetLauncher;
+    @Mock private Preference mPreference;
+    @Mock private Context mContext;
+    @Mock private PackageManager mPackageManager;
+    @Mock private UserManager mUserManager;
+    private ResolveInfo mFactoryResetAppResolveInfo;
+    private PackageInfo mFactoryResetAppPackageInfo;
 
-    private Context mContext;
     private FactoryResetPreferenceController mController;
 
     @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mShadowUserManager = ShadowUserManager.getShadow();
-
+    public void setUp() throws PackageManager.NameNotFoundException {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
         mController = new FactoryResetPreferenceController(mContext, FACTORY_RESET_KEY);
+        mFactoryResetAppResolveInfo = new ResolveInfo();
+        mFactoryResetAppResolveInfo.activityInfo = new ActivityInfo();
+        mFactoryResetAppResolveInfo.activityInfo.packageName = FACTORY_RESET_APP_PACKAGE;
+        mFactoryResetAppPackageInfo = new PackageInfo();
+        mFactoryResetAppPackageInfo.requestedPermissions =
+                new String[] {Manifest.permission.PREPARE_FACTORY_RESET};
+        mFactoryResetAppPackageInfo.requestedPermissionsFlags = new int[] {
+                PackageInfo.REQUESTED_PERMISSION_GRANTED
+        };
+        when(mPackageManager.resolveActivity(any(), anyInt()))
+                .thenReturn(mFactoryResetAppResolveInfo);
+        when(mPackageManager.getPackageInfo(anyString(), anyInt()))
+                .thenReturn(mFactoryResetAppPackageInfo);
+        when(mPreference.getKey()).thenReturn(FACTORY_RESET_KEY);
+        mController.mFactoryResetPreparationLauncher = mFactoryResetLauncher;
+
     }
 
     @After
     public void tearDown() {
-        ShadowUtils.reset();
-        mShadowUserManager.setIsAdminUser(false);
-        mShadowUserManager.setIsDemoUser(false);
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0);
+        Mockito.reset(mUserManager, mPackageManager);
+        Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(),
+                Settings.Global.DEVICE_DEMO_MODE, 0);
     }
 
     @Ignore("b/314930928")
     @Test
     public void isAvailable_systemUser() {
-        mShadowUserManager.setIsAdminUser(true);
+        when(mUserManager.isAdminUser()).thenReturn(true);
 
         assertThat(mController.isAvailable()).isTrue();
     }
 
     @Test
     public void isAvailable_nonSystemUser() {
-        mShadowUserManager.setIsAdminUser(false);
-        mShadowUserManager.setIsDemoUser(false);
+        when(mUserManager.isAdminUser()).thenReturn(false);
+        when(mUserManager.isDemoUser()).thenReturn(false);
 
         assertThat(mController.isAvailable()).isFalse();
     }
 
     @Test
     public void isAvailable_demoUser() {
-        mShadowUserManager.setIsAdminUser(false);
+        when(mUserManager.isAdminUser()).thenReturn(false);
 
         // Place the device in demo mode.
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 1);
+        Settings.Global.putInt(RuntimeEnvironment.application.getContentResolver(),
+                Settings.Global.DEVICE_DEMO_MODE, 1);
 
         // Indicate the user is a demo user.
-        mShadowUserManager.addUser(UserHandle.myUserId(), "test", UserInfo.FLAG_DEMO);
+        when(mUserManager.getUserProfiles())
+                .thenReturn(ImmutableList.of(new UserHandle(UserHandle.myUserId())));
+        when(mUserManager.getUserInfo(eq(UserHandle.myUserId())))
+                .thenReturn(new UserInfo(UserHandle.myUserId(), "test", UserInfo.FLAG_DEMO));
 
         assertThat(mController.isAvailable()).isFalse();
     }
@@ -94,4 +144,16 @@
     public void getPreferenceKey() {
         assertThat(mController.getPreferenceKey()).isEqualTo(FACTORY_RESET_KEY);
     }
+
+    @Test
+    @RequiresFlagsEnabled(com.android.settings.factory_reset.Flags.FLAG_ENABLE_FACTORY_RESET_WIZARD)
+    public void handlePreference_factoryResetWizardEnabled() {
+        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue();
+        verify(mFactoryResetLauncher).launch(intentArgumentCaptor.capture());
+        assertThat(intentArgumentCaptor.getValue()).isNotNull();
+        assertThat(intentArgumentCaptor.getValue().getAction())
+                .isEqualTo(FactoryResetPreferenceController.ACTION_PREPARE_FACTORY_RESET);
+    }
 }
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
index 6b4cc0d..2afb3f1 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
@@ -136,8 +136,7 @@
 
         verify(packageInstaller).requestArchive(
             eq(PACKAGE_NAME),
-            any(),
-            eq(0)
+            any()
         )
     }