Merge "Add progress bar." into main
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index 0534c6e..a0f916c 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -17,6 +17,7 @@
 <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/root"
     style="@style/Widget.SliceView.Panel"
     android:layout_width="wrap_content"
@@ -53,17 +54,40 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_title" />
 
+    <View
+        android:id="@+id/bluetooth_tile_dialog_progress_background"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintEnd_toEndOf="@id/bluetooth_tile_dialog_progress_animation"
+        app:layout_constraintStart_toStartOf="@id/bluetooth_tile_dialog_progress_animation"
+        app:layout_constraintTop_toTopOf="@id/bluetooth_tile_dialog_progress_animation"
+        app:layout_constraintBottom_toBottomOf="@id/bluetooth_tile_dialog_progress_animation"
+        android:background="?androidprv:attr/colorSurfaceVariant" />
+
+    <ProgressBar
+        android:id="@+id/bluetooth_tile_dialog_progress_animation"
+        android:layout_width="152dp"
+        android:layout_height="4dp"
+        android:layout_marginTop="16dp"
+        style="@style/TrimmedHorizontalProgressBar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle"
+        android:visibility="invisible"
+        android:indeterminate="true" />
+
     <androidx.core.widget.NestedScrollView
         android:id="@+id/scroll_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="21dp"
+        android:minHeight="145dp"
         android:fillViewport="true"
         app:layout_constrainedHeight="true"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle">
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_progress_animation">
 
         <androidx.constraintlayout.widget.ConstraintLayout
             android:id="@+id/scroll_layout"
@@ -81,7 +105,7 @@
                 android:paddingStart="36dp"
                 android:text="@string/turn_on_bluetooth"
                 android:clickable="false"
-                android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+                android:textAppearance="@style/TextAppearance.Dialog.Title"
                 android:textSize="16sp"
                 app:layout_constraintEnd_toStartOf="@+id/bluetooth_toggle"
                 app:layout_constraintStart_toStartOf="parent"
@@ -90,8 +114,7 @@
             <Switch
                 android:id="@+id/bluetooth_toggle"
                 android:layout_width="wrap_content"
-                android:layout_height="48dp"
-                android:paddingTop="10dp"
+                android:layout_height="64dp"
                 android:gravity="start|center_vertical"
                 android:paddingEnd="40dp"
                 android:contentDescription="@string/turn_on_bluetooth"
@@ -107,7 +130,6 @@
                 android:id="@+id/device_list"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="20dp"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index 1805eb1..1a06c38 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -22,12 +22,14 @@
 import android.view.View
 import android.view.View.AccessibilityDelegate
 import android.view.View.GONE
+import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.ImageView
+import android.widget.ProgressBar
 import android.widget.Switch
 import android.widget.TextView
 import androidx.recyclerview.widget.AsyncListDiffer
@@ -92,6 +94,8 @@
     private lateinit var pairNewDeviceButton: View
     private lateinit var deviceListView: RecyclerView
     private lateinit var scrollViewContent: View
+    private lateinit var progressBarAnimation: ProgressBar
+    private lateinit var progressBarBackground: View
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -122,6 +126,8 @@
             scrollViewContent = this
             layoutParams.height = cachedContentHeight
         }
+        progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+        progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background)
     }
 
     override fun start() {
@@ -135,6 +141,17 @@
         super.dismiss()
     }
 
+    internal suspend fun animateProgressBar(animate: Boolean) {
+        withContext(mainDispatcher) {
+            if (animate) {
+                showProgressBar()
+            } else {
+                delay(PROGRESS_BAR_ANIMATION_DURATION_MS)
+                hideProgressBar()
+            }
+        }
+    }
+
     internal suspend fun onDeviceItemUpdated(
         deviceItem: List<DeviceItem>,
         showSeeAll: Boolean,
@@ -190,6 +207,28 @@
         }
     }
 
+    private fun showProgressBar() {
+        if (
+            ::progressBarAnimation.isInitialized &&
+                ::progressBarBackground.isInitialized &&
+                progressBarAnimation.visibility != VISIBLE
+        ) {
+            progressBarAnimation.visibility = VISIBLE
+            progressBarBackground.visibility = INVISIBLE
+        }
+    }
+
+    private fun hideProgressBar() {
+        if (
+            ::progressBarAnimation.isInitialized &&
+                ::progressBarBackground.isInitialized &&
+                progressBarAnimation.visibility != INVISIBLE
+        ) {
+            progressBarAnimation.visibility = INVISIBLE
+            progressBarBackground.visibility = VISIBLE
+        }
+    }
+
     internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
         RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
 
@@ -300,6 +339,7 @@
         const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
         const val DISABLED_ALPHA = 0.3f
         const val ENABLED_ALPHA = 1f
+        const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L
 
         private fun Boolean.toInt(): Int {
             return if (this) 1 else 0
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index 6d08f59..194e7bc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -105,6 +105,41 @@
                     deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD)
                 }
 
+                // deviceItemUpdate is emitted when device item list is done fetching, update UI and
+                // stop the progress bar.
+                deviceItemInteractor.deviceItemUpdate
+                    .onEach {
+                        updateDialogUiJob?.cancel()
+                        updateDialogUiJob = launch {
+                            dialog.apply {
+                                onDeviceItemUpdated(
+                                    it.take(MAX_DEVICE_ITEM_ENTRY),
+                                    showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY,
+                                    showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled
+                                )
+                                animateProgressBar(false)
+                            }
+                        }
+                    }
+                    .launchIn(this)
+
+                // deviceItemUpdateRequest is emitted when a bluetooth callback is called, re-fetch
+                // the device item list and animiate the progress bar.
+                deviceItemInteractor.deviceItemUpdateRequest
+                    .onEach {
+                        dialog.animateProgressBar(true)
+                        updateDeviceItemJob?.cancel()
+                        updateDeviceItemJob = launch {
+                            deviceItemInteractor.updateDeviceItems(
+                                context,
+                                DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
+                            )
+                        }
+                    }
+                    .launchIn(this)
+
+                // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch
+                // the device item list.
                 bluetoothStateInteractor.bluetoothStateUpdate
                     .filterNotNull()
                     .onEach {
@@ -119,39 +154,21 @@
                     }
                     .launchIn(this)
 
-                deviceItemInteractor.deviceItemUpdateRequest
-                    .onEach {
-                        updateDeviceItemJob?.cancel()
-                        updateDeviceItemJob = launch {
-                            deviceItemInteractor.updateDeviceItems(
-                                context,
-                                DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
-                            )
-                        }
-                    }
-                    .launchIn(this)
-
-                deviceItemInteractor.deviceItemUpdate
-                    .onEach {
-                        updateDialogUiJob?.cancel()
-                        updateDialogUiJob = launch {
-                            dialog.onDeviceItemUpdated(
-                                it.take(MAX_DEVICE_ITEM_ENTRY),
-                                showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY,
-                                showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled
-                            )
-                        }
-                    }
-                    .launchIn(this)
-
+                // bluetoothStateToggle is emitted when user toggles the bluetooth state switch,
+                // send the new value to the bluetoothStateInteractor and animate the progress bar.
                 dialog.bluetoothStateToggle
-                    .onEach { bluetoothStateInteractor.isBluetoothEnabled = it }
+                    .onEach {
+                        dialog.animateProgressBar(true)
+                        bluetoothStateInteractor.isBluetoothEnabled = it
+                    }
                     .launchIn(this)
 
+                // deviceItemClick is emitted when user clicked on a device item.
                 dialog.deviceItemClick
                     .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) }
                     .launchIn(this)
 
+                // contentHeight is emitted when the dialog is dismissed.
                 dialog.contentHeight
                     .onEach {
                         withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 33066d2..9563ceb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -162,13 +162,15 @@
 
     @Test
     fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
-        `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
-        bluetoothTileDialogViewModel.showDialog(context, null)
+        testScope.runTest {
+            `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+            bluetoothTileDialogViewModel.showDialog(context, null)
 
-        val clickedView = View(context)
-        bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
+            val clickedView = View(context)
+            bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
 
-        verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
-        verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
+            verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
+            verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
+        }
     }
 }