Merge "Fix media control flash when unlock device" into tm-qpr-dev
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 14fe522..3e6283e 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1132,12 +1132,26 @@
      * @return the dimensions of system wallpaper
      * @hide
      */
+    @Nullable
     public Rect peekBitmapDimensions() {
         return sGlobals.peekWallpaperDimensions(
                 mContext, true /* returnDefault */, mContext.getUserId());
     }
 
     /**
+     * Peek the dimensions of given wallpaper of the user without decoding it.
+     *
+     * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or
+     *     {@link #FLAG_LOCK}.
+     * @return the dimensions of system wallpaper
+     * @hide
+     */
+    @Nullable
+    public Rect peekBitmapDimensions(@SetWallpaperFlags int which) {
+        return peekBitmapDimensions();
+    }
+
+    /**
      * Get an open, readable file descriptor to the given wallpaper image file.
      * The caller is responsible for closing the file descriptor when done ingesting the file.
      *
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml
new file mode 100644
index 0000000..e850d68
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 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="?attr/colorControlNormal">
+  <path
+      android:fillColor="#1f1f1f"
+      android:pathData="M8,22V11L6,8V2H18V8L16,11V22ZM12,15.5Q11.375,15.5 10.938,15.062Q10.5,14.625 10.5,14Q10.5,13.375 10.938,12.938Q11.375,12.5 12,12.5Q12.625,12.5 13.062,12.938Q13.5,13.375 13.5,14Q13.5,14.625 13.062,15.062Q12.625,15.5 12,15.5ZM8,5H16V4H8ZM16,7H8V7.4L10,10.4V20H14V10.4L16,7.4ZM12,12Z"/>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml
new file mode 100644
index 0000000..91b9ae5
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2022 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="?attr/colorControlNormal">
+  <path
+      android:fillColor="#1f1f1f"
+      android:pathData="M6,5V2H18V5ZM12,15.5Q12.625,15.5 13.062,15.062Q13.5,14.625 13.5,14Q13.5,13.375 13.062,12.938Q12.625,12.5 12,12.5Q11.375,12.5 10.938,12.938Q10.5,13.375 10.5,14Q10.5,14.625 10.938,15.062Q11.375,15.5 12,15.5ZM8,22V11L6,8V7H18V8L16,11V22Z"/>
+</vector>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index 4dfcd63..66e5d7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -30,6 +30,7 @@
 import android.service.controls.ControlsProviderService
 import androidx.annotation.WorkerThread
 import com.android.settingslib.applications.DefaultAppInfo
+import com.android.systemui.R
 import java.util.Objects
 
 class ControlsServiceInfo(
@@ -59,7 +60,8 @@
      * instead of using the controls rendered by SystemUI.
      *
      * The activity must be in the same package, exported, enabled and protected by the
-     * [Manifest.permission.BIND_CONTROLS] permission.
+     * [Manifest.permission.BIND_CONTROLS] permission. Additionally, only packages declared in
+     * [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
      */
     var panelActivity: ComponentName? = null
         private set
@@ -70,6 +72,9 @@
     fun resolvePanelActivity() {
         if (resolved) return
         resolved = true
+        val validPackages = context.resources
+                .getStringArray(R.array.config_controlsPreferredPackages)
+        if (componentName.packageName !in validPackages) return
         panelActivity = _panelActivity?.let {
             val resolveInfos = mPm.queryIntentActivitiesAsUser(
                     Intent().setComponent(it),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index f5220b8..73dbeab 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -25,6 +25,7 @@
 object BuiltInKeyguardQuickAffordanceKeys {
     // Please keep alphabetical order of const names to simplify future maintenance.
     const val CAMERA = "camera"
+    const val FLASHLIGHT = "flashlight"
     const val HOME_CONTROLS = "home"
     const val QR_CODE_SCANNER = "qr_code_scanner"
     const val QUICK_ACCESS_WALLET = "wallet"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
new file mode 100644
index 0000000..49527d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
@@ -0,0 +1,144 @@
+/*
+ *  Copyright (C) 2022 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.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.statusbar.policy.FlashlightController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class FlashlightQuickAffordanceConfig @Inject constructor(
+        @Application private val context: Context,
+        private val flashlightController: FlashlightController,
+) : KeyguardQuickAffordanceConfig {
+
+    private sealed class FlashlightState {
+
+        abstract fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState
+
+        object On: FlashlightState() {
+            override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.ic_flashlight_on,
+                        ContentDescription.Resource(R.string.quick_settings_flashlight_label)
+                    ),
+                    ActivationState.Active
+                )
+        }
+
+        object OffAvailable: FlashlightState() {
+            override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    Icon.Resource(
+                        R.drawable.ic_flashlight_off,
+                        ContentDescription.Resource(R.string.quick_settings_flashlight_label)
+                    ),
+                    ActivationState.Inactive
+                )
+        }
+
+        object Unavailable: FlashlightState() {
+            override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+                KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+        }
+    }
+
+    override val key: String
+        get() = BuiltInKeyguardQuickAffordanceKeys.FLASHLIGHT
+
+    override val pickerName: String
+        get() = context.getString(R.string.quick_settings_flashlight_label)
+
+    override val pickerIconResourceId: Int
+        get() = if (flashlightController.isEnabled) {
+            R.drawable.ic_flashlight_on
+        } else {
+            R.drawable.ic_flashlight_off
+        }
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+            conflatedCallbackFlow {
+        val flashlightCallback = object : FlashlightController.FlashlightListener {
+            override fun onFlashlightChanged(enabled: Boolean) {
+                trySendWithFailureLogging(
+                    if (enabled) {
+                        FlashlightState.On.toLockScreenState()
+                    } else {
+                        FlashlightState.OffAvailable.toLockScreenState()
+                    },
+                    TAG
+                )
+            }
+
+            override fun onFlashlightError() {
+                trySendWithFailureLogging(FlashlightState.OffAvailable.toLockScreenState(), TAG)
+            }
+
+            override fun onFlashlightAvailabilityChanged(available: Boolean) {
+                trySendWithFailureLogging(
+                    if (!available) {
+                        FlashlightState.Unavailable.toLockScreenState()
+                    } else {
+                        if (flashlightController.isEnabled) {
+                            FlashlightState.On.toLockScreenState()
+                        } else {
+                            FlashlightState.OffAvailable.toLockScreenState()
+                        }
+                    },
+                    TAG
+                )
+            }
+        }
+
+        flashlightController.addCallback(flashlightCallback)
+
+        awaitClose {
+            flashlightController.removeCallback(flashlightCallback)
+        }
+    }
+
+    override fun onTriggered(expandable: Expandable?):
+            KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        flashlightController
+                .setFlashlight(flashlightController.isAvailable && !flashlightController.isEnabled)
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+
+    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
+        if (flashlightController.isAvailable) {
+            KeyguardQuickAffordanceConfig.PickerScreenState.Default
+        } else {
+            KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+        }
+
+    companion object {
+        private const val TAG = "FlashlightQuickAffordanceConfig"
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index f7225a2..3013227c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -26,6 +26,7 @@
     @Provides
     @ElementsIntoSet
     fun quickAffordanceConfigs(
+        flashlight: FlashlightQuickAffordanceConfig,
         home: HomeControlsKeyguardQuickAffordanceConfig,
         quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
         qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
@@ -33,6 +34,7 @@
     ): Set<KeyguardQuickAffordanceConfig> {
         return setOf(
             camera,
+            flashlight,
             home,
             quickAccessWallet,
             qrCodeScanner,
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index f9e341c..d6e29e0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.log
 
-import android.app.ActivityManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogcatEchoTracker
 
@@ -29,15 +29,6 @@
     private val dumpManager: DumpManager,
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
-    /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
-    private fun adjustMaxSize(requestedMaxSize: Int): Int {
-        return if (ActivityManager.isLowRamDeviceStatic()) {
-            minOf(requestedMaxSize, 20) /* low ram max log size*/
-        } else {
-            requestedMaxSize
-        }
-    }
-
     @JvmOverloads
     fun create(
         name: String,
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt
new file mode 100644
index 0000000..619eac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 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.systemui.log
+
+import android.app.ActivityManager
+
+class LogBufferHelper {
+    companion object {
+        /** If necessary, returns a limited maximum size for low ram (Go) devices */
+        fun adjustMaxSize(requestedMaxSize: Int): Int {
+            return if (ActivityManager.isLowRamDeviceStatic()) {
+                minOf(requestedMaxSize, 20) /* low ram max log size*/
+            } else {
+                requestedMaxSize
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
new file mode 100644
index 0000000..c27bfa3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+import com.android.systemui.util.kotlin.pairwiseBy
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * An interface that enables logging the difference between values in table format.
+ *
+ * Many objects that we want to log are data-y objects with a collection of fields. When logging
+ * these objects, we want to log each field separately. This allows ABT (Android Bug Tool) to easily
+ * highlight changes in individual fields.
+ *
+ * See [TableLogBuffer].
+ */
+interface Diffable<T> {
+    /**
+     * Finds the differences between [prevVal] and [this] and logs those diffs to [row].
+     *
+     * Each implementer should determine which individual fields have changed between [prevVal] and
+     * [this], and only log the fields that have actually changed. This helps save buffer space.
+     *
+     * For example, if:
+     * - prevVal = Object(val1=100, val2=200, val3=300)
+     * - this = Object(val1=100, val2=200, val3=333)
+     *
+     * Then only the val3 change should be logged.
+     */
+    fun logDiffs(prevVal: T, row: TableRowLogger)
+}
+
+/**
+ * Each time the flow is updated with a new value, logs the differences between the previous value
+ * and the new value to the given [tableLogBuffer].
+ *
+ * The new value's [Diffable.logDiffs] method will be used to log the differences to the table.
+ *
+ * @param columnPrefix a prefix that will be applied to every column name that gets logged.
+ */
+fun <T : Diffable<T>> Flow<T>.logDiffsForTable(
+    tableLogBuffer: TableLogBuffer,
+    columnPrefix: String,
+    initialValue: T,
+): Flow<T> {
+    return this.pairwiseBy(initialValue) { prevVal, newVal ->
+        tableLogBuffer.logDiffs(columnPrefix, prevVal, newVal)
+        newVal
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
new file mode 100644
index 0000000..68c297f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+/**
+ * A object used with [TableLogBuffer] to store changes in variables over time. Is recyclable.
+ *
+ * Each message represents a change to exactly 1 type, specified by [DataType].
+ */
+data class TableChange(
+    var timestamp: Long = 0,
+    var columnPrefix: String = "",
+    var columnName: String = "",
+    var type: DataType = DataType.EMPTY,
+    var bool: Boolean = false,
+    var int: Int = 0,
+    var str: String? = null,
+) {
+    /** Resets to default values so that the object can be recycled. */
+    fun reset(timestamp: Long, columnPrefix: String, columnName: String) {
+        this.timestamp = timestamp
+        this.columnPrefix = columnPrefix
+        this.columnName = columnName
+        this.type = DataType.EMPTY
+        this.bool = false
+        this.int = 0
+        this.str = null
+    }
+
+    /** Sets this to store a string change. */
+    fun set(value: String?) {
+        type = DataType.STRING
+        str = value
+    }
+
+    /** Sets this to store a boolean change. */
+    fun set(value: Boolean) {
+        type = DataType.BOOLEAN
+        bool = value
+    }
+
+    /** Sets this to store an int change. */
+    fun set(value: Int) {
+        type = DataType.INT
+        int = value
+    }
+
+    /** Returns true if this object has a change. */
+    fun hasData(): Boolean {
+        return columnName.isNotBlank() && type != DataType.EMPTY
+    }
+
+    fun getName(): String {
+        return if (columnPrefix.isNotBlank()) {
+            "$columnPrefix.$columnName"
+        } else {
+            columnName
+        }
+    }
+
+    fun getVal(): String {
+        return when (type) {
+            DataType.EMPTY -> null
+            DataType.STRING -> str
+            DataType.INT -> int
+            DataType.BOOLEAN -> bool
+        }.toString()
+    }
+
+    enum class DataType {
+        STRING,
+        BOOLEAN,
+        INT,
+        EMPTY,
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
new file mode 100644
index 0000000..429637a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.plugins.util.RingBuffer
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A logger that logs changes in table format.
+ *
+ * Some parts of System UI maintain a lot of pieces of state at once.
+ * [com.android.systemui.plugins.log.LogBuffer] allows us to easily log change events:
+ *
+ * - 10-10 10:10:10.456: state2 updated to newVal2
+ * - 10-10 10:11:00.000: stateN updated to StateN(val1=true, val2=1)
+ * - 10-10 10:11:02.123: stateN updated to StateN(val1=true, val2=2)
+ * - 10-10 10:11:05.123: state1 updated to newVal1
+ * - 10-10 10:11:06.000: stateN updated to StateN(val1=false, val2=3)
+ *
+ * However, it can sometimes be more useful to view the state changes in table format:
+ *
+ * - timestamp--------- | state1- | state2- | ... | stateN.val1 | stateN.val2
+ * - -------------------------------------------------------------------------
+ * - 10-10 10:10:10.123 | val1--- | val2--- | ... | false------ | 0-----------
+ * - 10-10 10:10:10.456 | val1--- | newVal2 | ... | false------ | 0-----------
+ * - 10-10 10:11:00.000 | val1--- | newVal2 | ... | true------- | 1-----------
+ * - 10-10 10:11:02.123 | val1--- | newVal2 | ... | true------- | 2-----------
+ * - 10-10 10:11:05.123 | newVal1 | newVal2 | ... | true------- | 2-----------
+ * - 10-10 10:11:06.000 | newVal1 | newVal2 | ... | false------ | 3-----------
+ *
+ * This class enables easy logging of the state changes in both change event format and table
+ * format.
+ *
+ * This class also enables easy logging of states that are a collection of fields. For example,
+ * stateN in the above example consists of two fields -- val1 and val2. It's useful to put each
+ * field into its own column so that ABT (Android Bug Tool) can easily highlight changes to
+ * individual fields.
+ *
+ * How it works:
+ *
+ * 1) Create an instance of this buffer via [TableLogBufferFactory].
+ *
+ * 2) For any states being logged, implement [Diffable]. Implementing [Diffable] allows the state to
+ * only log the fields that have *changed* since the previous update, instead of always logging all
+ * fields.
+ *
+ * 3) Each time a change in a state happens, call [logDiffs]. If your state is emitted using a
+ * [Flow], you should use the [logDiffsForTable] extension function to automatically log diffs any
+ * time your flow emits a new value.
+ *
+ * When a dump occurs, there will be two dumps:
+ *
+ * 1) The change events under the dumpable name "$name-changes".
+ *
+ * 2) This class will coalesce all the diffs into a table format and log them under the dumpable
+ * name "$name-table".
+ *
+ * @param maxSize the maximum size of the buffer. Must be > 0.
+ */
+class TableLogBuffer(
+    maxSize: Int,
+    private val name: String,
+    private val systemClock: SystemClock,
+) {
+    init {
+        if (maxSize <= 0) {
+            throw IllegalArgumentException("maxSize must be > 0")
+        }
+    }
+
+    private val buffer = RingBuffer(maxSize) { TableChange() }
+
+    // A [TableRowLogger] object, re-used each time [logDiffs] is called.
+    // (Re-used to avoid object allocation.)
+    private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this)
+
+    /**
+     * Log the differences between [prevVal] and [newVal].
+     *
+     * The [newVal] object's method [Diffable.logDiffs] will be used to fetch the diffs.
+     *
+     * @param columnPrefix a prefix that will be applied to every column name that gets logged. This
+     * ensures that all the columns related to the same state object will be grouped together in the
+     * table.
+     */
+    @Synchronized
+    fun <T : Diffable<T>> logDiffs(columnPrefix: String, prevVal: T, newVal: T) {
+        val row = tempRow
+        row.timestamp = systemClock.currentTimeMillis()
+        row.columnPrefix = columnPrefix
+        newVal.logDiffs(prevVal, row)
+    }
+
+    // Keep these individual [logChange] methods private (don't let clients give us their own
+    // timestamps.)
+
+    private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) {
+        val change = obtain(timestamp, prefix, columnName)
+        change.set(value)
+    }
+
+    private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) {
+        val change = obtain(timestamp, prefix, columnName)
+        change.set(value)
+    }
+
+    private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int) {
+        val change = obtain(timestamp, prefix, columnName)
+        change.set(value)
+    }
+
+    // TODO(b/259454430): Add additional change types here.
+
+    @Synchronized
+    private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange {
+        val tableChange = buffer.advance()
+        tableChange.reset(timestamp, prefix, columnName)
+        return tableChange
+    }
+
+    /**
+     * Registers this buffer as dumpables in [dumpManager]. Must be called for the table to be
+     * dumped.
+     *
+     * This will be automatically called in [TableLogBufferFactory.create].
+     */
+    fun registerDumpables(dumpManager: DumpManager) {
+        dumpManager.registerNormalDumpable("$name-changes", changeDumpable)
+        dumpManager.registerNormalDumpable("$name-table", tableDumpable)
+    }
+
+    private val changeDumpable = Dumpable { pw, _ -> dumpChanges(pw) }
+    private val tableDumpable = Dumpable { pw, _ -> dumpTable(pw) }
+
+    /** Dumps the list of [TableChange] objects. */
+    @Synchronized
+    @VisibleForTesting
+    fun dumpChanges(pw: PrintWriter) {
+        for (i in 0 until buffer.size) {
+            buffer[i].dump(pw)
+        }
+    }
+
+    /** Dumps an individual [TableChange]. */
+    private fun TableChange.dump(pw: PrintWriter) {
+        if (!this.hasData()) {
+            return
+        }
+        val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(timestamp)
+        pw.print(formattedTimestamp)
+        pw.print(" ")
+        pw.print(this.getName())
+        pw.print("=")
+        pw.print(this.getVal())
+        pw.println()
+    }
+
+    /**
+     * Coalesces all the [TableChange] objects into a table of values of time and dumps the table.
+     */
+    // TODO(b/259454430): Since this is an expensive process, it could cause the bug report dump to
+    //   fail and/or not dump anything else. We should move this processing to ABT (Android Bug
+    //   Tool), where we have unlimited time to process.
+    @Synchronized
+    @VisibleForTesting
+    fun dumpTable(pw: PrintWriter) {
+        val messages = buffer.iterator().asSequence().toList()
+
+        if (messages.isEmpty()) {
+            return
+        }
+
+        // Step 1: Create list of column headers
+        val headerSet = mutableSetOf<String>()
+        messages.forEach { headerSet.add(it.getName()) }
+        val headers: MutableList<String> = headerSet.toList().sorted().toMutableList()
+        headers.add(0, "timestamp")
+
+        // Step 2: Create a list with the current values for each column. Will be updated with each
+        // change.
+        val currentRow: MutableList<String> = MutableList(headers.size) { DEFAULT_COLUMN_VALUE }
+
+        // Step 3: For each message, make the correct update to [currentRow] and save it to [rows].
+        val columnIndices: Map<String, Int> =
+            headers.mapIndexed { index, headerName -> headerName to index }.toMap()
+        val allRows = mutableListOf<List<String>>()
+
+        messages.forEach {
+            if (!it.hasData()) {
+                return@forEach
+            }
+
+            val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(it.timestamp)
+            if (formattedTimestamp != currentRow[0]) {
+                // The timestamp has updated, so save the previous row and continue to the next row
+                allRows.add(currentRow.toList())
+                currentRow[0] = formattedTimestamp
+            }
+            val columnIndex = columnIndices[it.getName()]!!
+            currentRow[columnIndex] = it.getVal()
+        }
+        // Add the last row
+        allRows.add(currentRow.toList())
+
+        // Step 4: Dump the rows
+        DumpsysTableLogger(
+                name,
+                headers,
+                allRows,
+            )
+            .printTableData(pw)
+    }
+
+    /**
+     * A private implementation of [TableRowLogger].
+     *
+     * Used so that external clients can't modify [timestamp].
+     */
+    private class TableRowLoggerImpl(
+        var timestamp: Long,
+        var columnPrefix: String,
+        val tableLogBuffer: TableLogBuffer,
+    ) : TableRowLogger {
+        /** Logs a change to a string value. */
+        override fun logChange(columnName: String, value: String?) {
+            tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+        }
+
+        /** Logs a change to a boolean value. */
+        override fun logChange(columnName: String, value: Boolean) {
+            tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+        }
+
+        /** Logs a change to an int value. */
+        override fun logChange(columnName: String, value: Int) {
+            tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+        }
+    }
+}
+
+val TABLE_LOG_DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+private const val DEFAULT_COLUMN_VALUE = "UNKNOWN"
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
new file mode 100644
index 0000000..f1f906f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+
+@SysUISingleton
+class TableLogBufferFactory
+@Inject
+constructor(
+    private val dumpManager: DumpManager,
+    private val systemClock: SystemClock,
+) {
+    fun create(
+        name: String,
+        maxSize: Int,
+    ): TableLogBuffer {
+        val tableBuffer = TableLogBuffer(adjustMaxSize(maxSize), name, systemClock)
+        tableBuffer.registerDumpables(dumpManager)
+        return tableBuffer
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt
new file mode 100644
index 0000000..a7ba13b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+/**
+ * A class that logs a row to [TableLogBuffer].
+ *
+ * Objects that implement [Diffable] will receive an instance of this class, and can log any changes
+ * to individual fields using the [logChange] methods. All logged changes will be associated with
+ * the same timestamp.
+ */
+interface TableRowLogger {
+    /** Logs a change to a string value. */
+    fun logChange(columnName: String, value: String?)
+
+    /** Logs a change to a boolean value. */
+    fun logChange(columnName: String, value: Boolean)
+
+    /** Logs a change to an int value. */
+    fun logChange(columnName: String, value: Int)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 31e4464..5e47d6d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -19,6 +19,7 @@
 import android.annotation.IdRes
 import android.app.StatusBarManager
 import android.content.res.Configuration
+import android.os.Bundle
 import android.os.Trace
 import android.os.Trace.TRACE_TAG_APP
 import android.util.Pair
@@ -34,6 +35,8 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -53,6 +56,7 @@
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
 import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_SHADE_HEADER
+import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.VariableDateView
 import com.android.systemui.statusbar.policy.VariableDateViewController
@@ -89,7 +93,8 @@
     private val dumpManager: DumpManager,
     private val featureFlags: FeatureFlags,
     private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
-    private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager
+    private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
+    private val demoModeController: DemoModeController
 ) : ViewController<View>(header), Dumpable {
 
     companion object {
@@ -126,7 +131,7 @@
     private lateinit var qsCarrierGroupController: QSCarrierGroupController
 
     private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon)
-    private val clock: TextView = header.findViewById(R.id.clock)
+    private val clock: Clock = header.findViewById(R.id.clock)
     private val date: TextView = header.findViewById(R.id.date)
     private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons)
     private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group)
@@ -212,6 +217,14 @@
         view.onApplyWindowInsets(insets)
     }
 
+    private val demoModeReceiver = object : DemoMode {
+        override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
+        override fun dispatchDemoCommand(command: String, args: Bundle) =
+            clock.dispatchDemoCommand(command, args)
+        override fun onDemoModeStarted() = clock.onDemoModeStarted()
+        override fun onDemoModeFinished() = clock.onDemoModeFinished()
+    }
+
     private val chipVisibilityListener: ChipVisibilityListener = object : ChipVisibilityListener {
         override fun onChipVisibilityRefreshed(visible: Boolean) {
             if (header is MotionLayout) {
@@ -300,6 +313,7 @@
 
         dumpManager.registerDumpable(this)
         configurationController.addCallback(configurationControllerListener)
+        demoModeController.addCallback(demoModeReceiver)
 
         updateVisibility()
         updateTransition()
@@ -309,6 +323,7 @@
         privacyIconsController.chipVisibilityListener = null
         dumpManager.unregisterDumpable(this::class.java.simpleName)
         configurationController.removeCallback(configurationControllerListener)
+        demoModeController.removeCallback(demoModeReceiver)
     }
 
     fun disable(state1: Int, state2: Int, animate: Boolean) {
@@ -521,4 +536,7 @@
             updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges)
         }
     }
+
+    @VisibleForTesting
+    internal fun simulateViewDetached() = this.onViewDetached()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index e68182e..629d3d4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2072,6 +2072,13 @@
                 mInitialTouchX = x;
                 initVelocityTracker();
                 trackMovement(event);
+                float qsExpansionFraction = computeQsExpansionFraction();
+                // Intercept the touch if QS is between fully collapsed and fully expanded state
+                if (qsExpansionFraction > 0.0 && qsExpansionFraction < 1.0) {
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: down action, QS partially expanded/collapsed");
+                    return true;
+                }
                 if (mKeyguardShowing
                         && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
                     // Dragging down on the lockscreen statusbar should prohibit other interactions
@@ -2324,6 +2331,13 @@
         if (!isFullyCollapsed()) {
             handleQsDown(event);
         }
+        // defer touches on QQS to shade while shade is collapsing. Added margin for error
+        // as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
+        if (computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
+            mShadeLog.logMotionEvent(event,
+                    "handleQsTouch: QQS touched while shade collapsing");
+            mQsTracking = false;
+        }
         if (!mQsExpandImmediate && mQsTracking) {
             onQsTouch(event);
             if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
@@ -2564,7 +2578,6 @@
         // Reset scroll position and apply that position to the expanded height.
         float height = mQsExpansionHeight;
         setQsExpansionHeight(height);
-        updateExpandedHeightToMaxHeight();
         mNotificationStackScrollLayoutController.checkSnoozeLeavebehind();
 
         // When expanding QS, let's authenticate the user if possible,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index fcd1b8a..0662fb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
@@ -32,6 +35,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 @Module
 abstract class StatusBarPipelineModule {
@@ -57,4 +61,15 @@
 
     @Binds
     abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
+
+    @Module
+    companion object {
+        @JvmStatic
+        @Provides
+        @SysUISingleton
+        @WifiTableLog
+        fun provideWifiTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("WifiTableLog", 100)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt
new file mode 100644
index 0000000..ac395a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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.systemui.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** Wifi logs in table format. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class WifiTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 062c3d1..8436b13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -17,12 +17,30 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.model
 
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.table.TableRowLogger
+import com.android.systemui.log.table.Diffable
 
 /** Provides information about the current wifi network. */
-sealed class WifiNetworkModel {
+sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
+
     /** A model representing that we have no active wifi network. */
     object Inactive : WifiNetworkModel() {
         override fun toString() = "WifiNetwork.Inactive"
+
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal is Inactive) {
+                return
+            }
+            row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+
+            if (prevVal is CarrierMerged) {
+                // The only difference between CarrierMerged and Inactive is the type
+                return
+            }
+
+            // When changing from Active to Inactive, we need to log diffs to all the fields.
+            logDiffsFromActiveToNotActive(prevVal as Active, row)
+        }
     }
 
     /**
@@ -33,6 +51,21 @@
      */
     object CarrierMerged : WifiNetworkModel() {
         override fun toString() = "WifiNetwork.CarrierMerged"
+
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal is CarrierMerged) {
+                return
+            }
+            row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+
+            if (prevVal is Inactive) {
+                // The only difference between CarrierMerged and Inactive is the type.
+                return
+            }
+
+            // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
+            logDiffsFromActiveToNotActive(prevVal as Active, row)
+        }
     }
 
     /** Provides information about an active wifi network. */
@@ -76,6 +109,41 @@
             }
         }
 
+        override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+            if (prevVal !is Active) {
+                row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+            }
+
+            if (prevVal !is Active || prevVal.networkId != networkId) {
+                row.logChange(COL_NETWORK_ID, networkId)
+            }
+            if (prevVal !is Active || prevVal.isValidated != isValidated) {
+                row.logChange(COL_VALIDATED, isValidated)
+            }
+            if (prevVal !is Active || prevVal.level != level) {
+                row.logChange(COL_LEVEL, level ?: LEVEL_DEFAULT)
+            }
+            if (prevVal !is Active || prevVal.ssid != ssid) {
+                row.logChange(COL_SSID, ssid)
+            }
+
+            // TODO(b/238425913): The passpoint-related values are frequently never used, so it
+            //   would be great to not log them when they're not used.
+            if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+                row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+            }
+            if (prevVal !is Active ||
+                prevVal.isOnlineSignUpForPasspointAccessPoint !=
+                isOnlineSignUpForPasspointAccessPoint) {
+                row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+            }
+            if (prevVal !is Active ||
+                prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+                row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+            }
+        }
+
+
         override fun toString(): String {
             // Only include the passpoint-related values in the string if we have them. (Most
             // networks won't have them so they'll be mostly clutter.)
@@ -101,4 +169,37 @@
             internal const val MAX_VALID_LEVEL = 4
         }
     }
+
+    internal fun logDiffsFromActiveToNotActive(prevActive: Active, row: TableRowLogger) {
+        row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+        row.logChange(COL_VALIDATED, false)
+        row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+        row.logChange(COL_SSID, null)
+
+        if (prevActive.isPasspointAccessPoint) {
+            row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+        }
+        if (prevActive.isOnlineSignUpForPasspointAccessPoint) {
+            row.logChange(COL_ONLINE_SIGN_UP, false)
+        }
+        if (prevActive.passpointProviderFriendlyName != null) {
+            row.logChange(COL_PASSPOINT_NAME, null)
+        }
+    }
 }
+
+const val TYPE_CARRIER_MERGED = "CarrierMerged"
+const val TYPE_INACTIVE = "Inactive"
+const val TYPE_ACTIVE = "Active"
+
+const val COL_NETWORK_TYPE = "type"
+const val COL_NETWORK_ID = "networkId"
+const val COL_VALIDATED = "isValidated"
+const val COL_LEVEL = "level"
+const val COL_SSID = "ssid"
+const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
+const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
+const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
+
+const val LEVEL_DEFAULT = -1
+const val NETWORK_ID_DEFAULT = -1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 93448c1d..a663536 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -36,6 +36,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
@@ -82,6 +85,7 @@
     broadcastDispatcher: BroadcastDispatcher,
     connectivityManager: ConnectivityManager,
     logger: ConnectivityPipelineLogger,
+    @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
     @Main mainExecutor: Executor,
     @Application scope: CoroutineScope,
     wifiManager: WifiManager?,
@@ -199,6 +203,12 @@
 
         awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
     }
+        .distinctUntilChanged()
+        .logDiffsForTable(
+            wifiTableLogBuffer,
+            columnPrefix = "wifiNetwork",
+            initialValue = WIFI_NETWORK_DEFAULT,
+        )
         // There will be multiple wifi icons in different places that will frequently
         // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures that
         // new subscribes will get the latest value immediately upon subscription. Otherwise, the
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
index 98ff8d1..c677f19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
@@ -31,6 +31,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.settingslib.applications.ServiceListing
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.dump.DumpManager
@@ -110,6 +111,12 @@
                 .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)
         mContext.setMockPackageManager(packageManager)
 
+        mContext.orCreateTestableResources
+                .addOverride(
+                        R.array.config_controlsPreferredPackages,
+                        arrayOf(componentName.packageName)
+                )
+
         // Return true by default, we'll test the false path
         `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true)
 
@@ -482,6 +489,35 @@
     }
 
     @Test
+    fun testPackageNotPreferred_nullPanel() {
+        mContext.orCreateTestableResources
+                .addOverride(R.array.config_controlsPreferredPackages, arrayOf<String>())
+
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
     fun testListingsNotModifiedByCallback() {
         // This test checks that if the list passed to the callback is modified, it has no effect
         // in the resulting services
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..cda7018
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -0,0 +1,193 @@
+/*
+ *  Copyright (C) 2022 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.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.statusbar.policy.FlashlightController
+import com.android.systemui.utils.leaks.FakeFlashlightController
+import com.android.systemui.utils.leaks.LeakCheckedTest
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
+
+    @Mock private lateinit var context: Context
+    private lateinit var flashlightController: FakeFlashlightController
+    private lateinit var underTest : FlashlightQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        injectLeakCheckedDependency(FlashlightController::class.java)
+        MockitoAnnotations.initMocks(this)
+
+        flashlightController = SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) as FakeFlashlightController
+        underTest = FlashlightQuickAffordanceConfig(context, flashlightController)
+    }
+
+    @Test
+    fun `flashlight is off -- triggered -- icon is on and active`() = runTest {
+        //given
+        flashlightController.isEnabled = false
+        flashlightController.isAvailable = true
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        underTest.onTriggered(null)
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+        assertEquals(R.drawable.ic_flashlight_on,
+                ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight is on -- triggered -- icon is off and inactive`() = runTest {
+        //given
+        flashlightController.isEnabled = true
+        flashlightController.isAvailable = true
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        underTest.onTriggered(null)
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+        assertEquals(R.drawable.ic_flashlight_off,
+                ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight is on -- receives error -- icon is off and inactive`() = runTest {
+        //given
+        flashlightController.isEnabled = true
+        flashlightController.isAvailable = false
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        flashlightController.onFlashlightError()
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+        assertEquals(R.drawable.ic_flashlight_off,
+                ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight availability now off -- hidden`() = runTest {
+        //given
+        flashlightController.isEnabled = true
+        flashlightController.isAvailable = false
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        flashlightController.onFlashlightAvailabilityChanged(false)
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight availability now on -- flashlight on -- inactive and icon off`() = runTest {
+        //given
+        flashlightController.isEnabled = true
+        flashlightController.isAvailable = false
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        flashlightController.onFlashlightAvailabilityChanged(true)
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+        assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Active)
+        assertEquals(R.drawable.ic_flashlight_on, (lastValue.icon as? Icon.Resource)?.res)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight availability now on -- flashlight off -- inactive and icon off`() = runTest {
+        //given
+        flashlightController.isEnabled = false
+        flashlightController.isAvailable = false
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+        //when
+        flashlightController.onFlashlightAvailabilityChanged(true)
+        val lastValue = values.last()
+
+        //then
+        assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+        assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Inactive)
+        assertEquals(R.drawable.ic_flashlight_off, (lastValue.icon as? Icon.Resource)?.res)
+        job.cancel()
+    }
+
+    @Test
+    fun `flashlight available -- picker state default`() = runTest {
+        //given
+        flashlightController.isAvailable = true
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+    }
+
+    @Test
+    fun `flashlight not available -- picker state unavailable`() = runTest {
+        //given
+        flashlightController.isAvailable = false
+
+        //when
+        val result = underTest.getPickerScreenState()
+
+        //then
+        assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
new file mode 100644
index 0000000..432764a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class TableChangeTest : SysuiTestCase() {
+
+    @Test
+    fun setString_isString() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set("fakeValue")
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getVal()).isEqualTo("fakeValue")
+    }
+
+    @Test
+    fun setBoolean_isBoolean() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set(true)
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getVal()).isEqualTo("true")
+    }
+
+    @Test
+    fun setInt_isInt() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set(8900)
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getVal()).isEqualTo("8900")
+    }
+
+    @Test
+    fun setThenReset_isEmpty() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set(8900)
+        underTest.reset(timestamp = 0, columnPrefix = "prefix", columnName = "name")
+
+        assertThat(underTest.hasData()).isFalse()
+        assertThat(underTest.getVal()).isEqualTo("null")
+    }
+
+    @Test
+    fun getName_hasPrefix() {
+        val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
+
+        assertThat(underTest.getName()).contains("fakePrefix")
+        assertThat(underTest.getName()).contains("fakeName")
+    }
+
+    @Test
+    fun getName_noPrefix() {
+        val underTest = TableChange(columnPrefix = "", columnName = "fakeName")
+
+        assertThat(underTest.getName()).contains("fakeName")
+    }
+
+    @Test
+    fun resetThenSet_hasNewValue() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "prefix", columnName = "original")
+        underTest.set("fakeValue")
+        underTest.reset(timestamp = 0, columnPrefix = "", columnName = "updated")
+        underTest.set(8900)
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getName()).contains("updated")
+        assertThat(underTest.getName()).doesNotContain("prefix")
+        assertThat(underTest.getName()).doesNotContain("original")
+        assertThat(underTest.getVal()).isEqualTo("8900")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
new file mode 100644
index 0000000..688c66a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2022 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.systemui.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class TableLogBufferTest : SysuiTestCase() {
+    private lateinit var underTest: TableLogBuffer
+
+    private lateinit var systemClock: FakeSystemClock
+    private lateinit var outputWriter: StringWriter
+
+    @Before
+    fun setup() {
+        systemClock = FakeSystemClock()
+        outputWriter = StringWriter()
+
+        underTest = TableLogBuffer(MAX_SIZE, NAME, systemClock)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun maxSizeZero_throwsException() {
+        TableLogBuffer(maxSize = 0, "name", systemClock)
+    }
+
+    @Test
+    fun dumpChanges_strChange_logsFromNext() {
+        systemClock.setCurrentTimeMillis(100L)
+
+        val prevDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("stringValChange", "prevStringVal")
+                }
+            }
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("stringValChange", "newStringVal")
+                }
+            }
+
+        underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).contains("prefix")
+        assertThat(dumpedString).contains("stringValChange")
+        assertThat(dumpedString).contains("newStringVal")
+        assertThat(dumpedString).doesNotContain("prevStringVal")
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+    }
+
+    @Test
+    fun dumpChanges_boolChange_logsFromNext() {
+        systemClock.setCurrentTimeMillis(100L)
+
+        val prevDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("booleanValChange", false)
+                }
+            }
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("booleanValChange", true)
+                }
+            }
+
+        underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).contains("prefix")
+        assertThat(dumpedString).contains("booleanValChange")
+        assertThat(dumpedString).contains("true")
+        assertThat(dumpedString).doesNotContain("false")
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+    }
+
+    @Test
+    fun dumpChanges_intChange_logsFromNext() {
+        systemClock.setCurrentTimeMillis(100L)
+
+        val prevDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("intValChange", 12345)
+                }
+            }
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("intValChange", 67890)
+                }
+            }
+
+        underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).contains("prefix")
+        assertThat(dumpedString).contains("intValChange")
+        assertThat(dumpedString).contains("67890")
+        assertThat(dumpedString).doesNotContain("12345")
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+    }
+
+    @Test
+    fun dumpChanges_noPrefix() {
+        systemClock.setCurrentTimeMillis(100L)
+
+        val prevDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("booleanValChange", false)
+                }
+            }
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("booleanValChange", true)
+                }
+            }
+
+        // WHEN there's a blank prefix
+        underTest.logDiffs("", prevDiffable, nextDiffable)
+
+        val dumpedString = dumpChanges()
+
+        // THEN the dump still works
+        assertThat(dumpedString).contains("booleanValChange")
+        assertThat(dumpedString).contains("true")
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+    }
+
+    @Test
+    fun dumpChanges_multipleChangesForSameColumn_logs() {
+        lateinit var valToDump: String
+
+        val diffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("valChange", valToDump)
+                }
+            }
+
+        systemClock.setCurrentTimeMillis(12000L)
+        valToDump = "stateValue12"
+        underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+        systemClock.setCurrentTimeMillis(20000L)
+        valToDump = "stateValue20"
+        underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+        systemClock.setCurrentTimeMillis(40000L)
+        valToDump = "stateValue40"
+        underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+        systemClock.setCurrentTimeMillis(45000L)
+        valToDump = "stateValue45"
+        underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).contains("valChange")
+        assertThat(dumpedString).contains("stateValue12")
+        assertThat(dumpedString).contains("stateValue20")
+        assertThat(dumpedString).contains("stateValue40")
+        assertThat(dumpedString).contains("stateValue45")
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(12000L))
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(20000L))
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(40000L))
+        assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(45000L))
+    }
+
+    @Test
+    fun dumpChanges_multipleChangesAtOnce_logs() {
+        systemClock.setCurrentTimeMillis(100L)
+
+        val prevDiffable = object : TestDiffable() {}
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("status", "in progress")
+                    row.logChange("connected", false)
+                }
+            }
+
+        underTest.logDiffs(columnPrefix = "", prevDiffable, nextDiffable)
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).contains("status")
+        assertThat(dumpedString).contains("in progress")
+        assertThat(dumpedString).contains("connected")
+        assertThat(dumpedString).contains("false")
+    }
+
+    @Test
+    fun dumpChanges_rotatesIfBufferIsFull() {
+        lateinit var valToDump: String
+
+        val prevDiffable = object : TestDiffable() {}
+        val nextDiffable =
+            object : TestDiffable() {
+                override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+                    row.logChange("status", valToDump)
+                }
+            }
+
+        for (i in 0 until MAX_SIZE + 3) {
+            valToDump = "testString[$i]"
+            underTest.logDiffs(columnPrefix = "", prevDiffable, nextDiffable)
+        }
+
+        val dumpedString = dumpChanges()
+
+        assertThat(dumpedString).doesNotContain("testString[0]")
+        assertThat(dumpedString).doesNotContain("testString[1]")
+        assertThat(dumpedString).doesNotContain("testString[2]")
+        assertThat(dumpedString).contains("testString[3]")
+        assertThat(dumpedString).contains("testString[${MAX_SIZE + 2}]")
+    }
+
+    private fun dumpChanges(): String {
+        underTest.dumpChanges(PrintWriter(outputWriter))
+        return outputWriter.toString()
+    }
+
+    private abstract class TestDiffable : Diffable<TestDiffable> {
+        override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {}
+    }
+}
+
+private const val NAME = "TestTableBuffer"
+private const val MAX_SIZE = 10
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index e1007fa..858d0e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -35,6 +35,8 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -50,10 +52,12 @@
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.VariableDateView
 import com.android.systemui.statusbar.policy.VariableDateViewController
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
@@ -104,7 +108,7 @@
     @Mock
     private lateinit var featureFlags: FeatureFlags
     @Mock
-    private lateinit var clock: TextView
+    private lateinit var clock: Clock
     @Mock
     private lateinit var date: VariableDateView
     @Mock
@@ -138,6 +142,7 @@
     private lateinit var qsConstraints: ConstraintSet
     @Mock
     private lateinit var largeScreenConstraints: ConstraintSet
+    @Mock private lateinit var demoModeController: DemoModeController
 
     @JvmField @Rule
     val mockitoRule = MockitoJUnit.rule()
@@ -146,10 +151,12 @@
     private lateinit var controller: LargeScreenShadeHeaderController
     private lateinit var carrierIconSlots: List<String>
     private val configurationController = FakeConfigurationController()
+    private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode>
 
     @Before
     fun setUp() {
-        whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock)
+        demoModeControllerCapture = argumentCaptor<DemoMode>()
+        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
         whenever(clock.context).thenReturn(mockedContext)
 
         whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
@@ -195,7 +202,8 @@
             dumpManager,
             featureFlags,
             qsCarrierGroupControllerBuilder,
-            combinedShadeHeadersConstraintManager
+            combinedShadeHeadersConstraintManager,
+            demoModeController
         )
         whenever(view.isAttachedToWindow).thenReturn(true)
         controller.init()
@@ -617,6 +625,21 @@
     }
 
     @Test
+    fun demoMode_attachDemoMode() {
+        verify(demoModeController).addCallback(capture(demoModeControllerCapture))
+        demoModeControllerCapture.value.onDemoModeStarted()
+        verify(clock).onDemoModeStarted()
+    }
+
+    @Test
+    fun demoMode_detachDemoMode() {
+        controller.simulateViewDetached()
+        verify(demoModeController).removeCallback(capture(demoModeControllerCapture))
+        demoModeControllerCapture.value.onDemoModeFinished()
+        verify(clock).onDemoModeFinished()
+    }
+
+    @Test
     fun animateOutOnStartCustomizing() {
         val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
         val duration = 1000L
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index 90ae693..b4c8f98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -13,6 +13,8 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -22,9 +24,12 @@
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.VariableDateViewController
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -52,7 +57,7 @@
     @Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
     @Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
     @Mock private lateinit var featureFlags: FeatureFlags
-    @Mock private lateinit var clock: TextView
+    @Mock private lateinit var clock: Clock
     @Mock private lateinit var date: TextView
     @Mock private lateinit var carrierGroup: QSCarrierGroup
     @Mock private lateinit var batteryMeterView: BatteryMeterView
@@ -66,6 +71,7 @@
         CombinedShadeHeadersConstraintManager
 
     @Mock private lateinit var mockedContext: Context
+    @Mock private lateinit var demoModeController: DemoModeController
 
     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
     var viewVisibility = View.GONE
@@ -76,7 +82,7 @@
 
     @Before
     fun setup() {
-        whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock)
+        whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
         whenever(clock.context).thenReturn(mockedContext)
         whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
         whenever(date.context).thenReturn(mockedContext)
@@ -111,8 +117,9 @@
                 dumpManager,
                 featureFlags,
                 qsCarrierGroupControllerBuilder,
-                combinedShadeHeadersConstraintManager
-        )
+                combinedShadeHeadersConstraintManager,
+                demoModeController
+                )
         whenever(view.isAttachedToWindow).thenReturn(true)
         mLargeScreenShadeHeaderController.init()
         carrierIconSlots = listOf(
@@ -230,4 +237,21 @@
         verify(animator).setInterpolator(Interpolators.ALPHA_IN)
         verify(animator).start()
     }
+
+    @Test
+    fun demoMode_attachDemoMode() {
+        val cb = argumentCaptor<DemoMode>()
+        verify(demoModeController).addCallback(capture(cb))
+        cb.value.onDemoModeStarted()
+        verify(clock).onDemoModeStarted()
+    }
+
+    @Test
+    fun demoMode_detachDemoMode() {
+        mLargeScreenShadeHeaderController.simulateViewDetached()
+        val cb = argumentCaptor<DemoMode>()
+        verify(demoModeController).removeCallback(capture(cb))
+        cb.value.onDemoModeFinished()
+        verify(clock).onDemoModeFinished()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 3d29d2b..30fd308 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -18,8 +18,10 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
 @SmallTest
@@ -48,6 +50,125 @@
         WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
     }
 
+    // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
+
+    @Test
+    fun logDiffs_inactiveToActive_logsAllActiveFields() {
+        val logger = TestLogger()
+        val activeNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 3,
+                ssid = "Test SSID"
+            )
+
+        activeNetwork.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "3"))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
+    }
+    @Test
+    fun logDiffs_activeToInactive_resetsAllActiveFields() {
+        val logger = TestLogger()
+        val activeNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 3,
+                ssid = "Test SSID"
+            )
+
+        WifiNetworkModel.Inactive.logDiffs(prevVal = activeNetwork, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
+    fun logDiffs_carrierMergedToActive_logsAllActiveFields() {
+        val logger = TestLogger()
+        val activeNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 3,
+                ssid = "Test SSID"
+            )
+
+        activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, "3"))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
+    }
+    @Test
+    fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+        val logger = TestLogger()
+        val activeNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 3,
+                ssid = "Test SSID"
+            )
+
+        WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+        assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+        assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+        assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+    }
+
+    @Test
+    fun logDiffs_activeChangesLevel_onlyLevelLogged() {
+        val logger = TestLogger()
+        val prevActiveNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 3,
+                ssid = "Test SSID"
+            )
+        val newActiveNetwork =
+            WifiNetworkModel.Active(
+                networkId = 5,
+                isValidated = true,
+                level = 2,
+                ssid = "Test SSID"
+            )
+
+        newActiveNetwork.logDiffs(prevActiveNetwork, logger)
+
+        assertThat(logger.changes).isEqualTo(listOf(Pair(COL_LEVEL, "2")))
+    }
+
+    private class TestLogger : TableRowLogger {
+        val changes = mutableListOf<Pair<String, String>>()
+
+        override fun logChange(columnName: String, value: String?) {
+            changes.add(Pair(columnName, value.toString()))
+        }
+
+        override fun logChange(columnName: String, value: Int) {
+            changes.add(Pair(columnName, value.toString()))
+        }
+
+        override fun logChange(columnName: String, value: Boolean) {
+            changes.add(Pair(columnName, value.toString()))
+        }
+    }
+
     companion object {
         private const val NETWORK_ID = 2
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index a64a4bd..800f3c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -29,6 +29,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
@@ -69,6 +70,7 @@
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
     private lateinit var executor: Executor
@@ -804,6 +806,7 @@
             broadcastDispatcher,
             connectivityManager,
             logger,
+            tableLogger,
             executor,
             scope,
             wifiManagerToUse,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
index f6fd2cb..f68baf5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
@@ -16,32 +16,71 @@
 
 import android.testing.LeakCheck;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.systemui.statusbar.policy.FlashlightController;
 import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class FakeFlashlightController extends BaseLeakChecker<FlashlightListener>
         implements FlashlightController {
+
+    private final List<FlashlightListener> callbacks = new ArrayList<>();
+
+    @VisibleForTesting
+    public boolean isAvailable;
+    @VisibleForTesting
+    public boolean isEnabled;
+    @VisibleForTesting
+    public boolean hasFlashlight;
+
     public FakeFlashlightController(LeakCheck test) {
         super(test, "flashlight");
     }
 
+    @VisibleForTesting
+    public void onFlashlightAvailabilityChanged(boolean newValue) {
+        callbacks.forEach(
+                flashlightListener -> flashlightListener.onFlashlightAvailabilityChanged(newValue)
+        );
+    }
+
+    @VisibleForTesting
+    public void onFlashlightError() {
+        callbacks.forEach(FlashlightListener::onFlashlightError);
+    }
+
     @Override
     public boolean hasFlashlight() {
-        return false;
+        return hasFlashlight;
     }
 
     @Override
     public void setFlashlight(boolean newState) {
-
+        callbacks.forEach(flashlightListener -> flashlightListener.onFlashlightChanged(newState));
     }
 
     @Override
     public boolean isAvailable() {
-        return false;
+        return isAvailable;
     }
 
     @Override
     public boolean isEnabled() {
-        return false;
+        return isEnabled;
+    }
+
+    @Override
+    public void addCallback(FlashlightListener listener) {
+        super.addCallback(listener);
+        callbacks.add(listener);
+    }
+
+    @Override
+    public void removeCallback(FlashlightListener listener) {
+        super.removeCallback(listener);
+        callbacks.remove(listener);
     }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index a92b65e..4d44c886 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -3796,13 +3796,13 @@
         }
 
         private void createNotificationChannelsImpl(String pkg, int uid,
-                ParceledListSlice channelsList, boolean fromTargetApp) {
-            createNotificationChannelsImpl(pkg, uid, channelsList, fromTargetApp,
+                ParceledListSlice channelsList) {
+            createNotificationChannelsImpl(pkg, uid, channelsList,
                     ActivityTaskManager.INVALID_TASK_ID);
         }
 
         private void createNotificationChannelsImpl(String pkg, int uid,
-                ParceledListSlice channelsList, boolean fromTargetApp, int startingTaskId) {
+                ParceledListSlice channelsList, int startingTaskId) {
             List<NotificationChannel> channels = channelsList.getList();
             final int channelsSize = channels.size();
             ParceledListSlice<NotificationChannel> oldChannels =
@@ -3814,7 +3814,7 @@
                 final NotificationChannel channel = channels.get(i);
                 Objects.requireNonNull(channel, "channel in list is null");
                 needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid,
-                        channel, fromTargetApp,
+                        channel, true /* fromTargetApp */,
                         mConditionProviders.isPackageOrComponentAllowed(
                                 pkg, UserHandle.getUserId(uid)));
                 if (needsPolicyFileChange) {
@@ -3850,7 +3850,6 @@
         @Override
         public void createNotificationChannels(String pkg, ParceledListSlice channelsList) {
             checkCallerIsSystemOrSameApp(pkg);
-            boolean fromTargetApp = !isCallerSystemOrPhone();  // if not system, it's from the app
             int taskId = ActivityTaskManager.INVALID_TASK_ID;
             try {
                 int uid = mPackageManager.getPackageUid(pkg, 0,
@@ -3859,15 +3858,14 @@
             } catch (RemoteException e) {
                 // Do nothing
             }
-            createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, fromTargetApp,
-                    taskId);
+            createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, taskId);
         }
 
         @Override
         public void createNotificationChannelsForPackage(String pkg, int uid,
                 ParceledListSlice channelsList) {
             enforceSystemOrSystemUI("only system can call this");
-            createNotificationChannelsImpl(pkg, uid, channelsList, false /* fromTargetApp */);
+            createNotificationChannelsImpl(pkg, uid, channelsList);
         }
 
         @Override
@@ -3882,8 +3880,7 @@
                     CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId));
             conversationChannel.setConversationId(parentId, conversationId);
             createNotificationChannelsImpl(
-                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)),
-                    false /* fromTargetApp */);
+                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)));
             mRankingHandler.requestSort();
             handleSavePolicyFile();
         }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 9791158..d8aa469 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -916,7 +916,7 @@
                 throw new IllegalArgumentException("Reserved id");
             }
             NotificationChannel existing = r.channels.get(channel.getId());
-            if (existing != null) {
+            if (existing != null && fromTargetApp) {
                 // Actually modifying an existing channel - keep most of the existing settings
                 if (existing.isDeleted()) {
                     // The existing channel was deleted - undelete it.
@@ -1002,7 +1002,9 @@
                 }
                 if (fromTargetApp) {
                     channel.setLockscreenVisibility(r.visibility);
-                    channel.setAllowBubbles(NotificationChannel.DEFAULT_ALLOW_BUBBLE);
+                    channel.setAllowBubbles(existing != null
+                            ? existing.getAllowBubbles()
+                            : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
                 }
                 clearLockedFieldsLocked(channel);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 077caa4..3f3b052 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -1101,10 +1101,6 @@
                 new NotificationChannel("id", "name", IMPORTANCE_HIGH);
         mBinderService.updateNotificationChannelForPackage(PKG, mUid, updatedChannel);
 
-        // pretend only this following part is called by the app (system permissions are required to
-        // update the notification channel on behalf of the user above)
-        mService.isSystemUid = false;
-
         // Recreating with a lower importance leaves channel unchanged.
         final NotificationChannel dupeChannel =
                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_LOW);
@@ -1130,46 +1126,6 @@
     }
 
     @Test
-    public void testCreateNotificationChannels_fromAppCannotSetFields() throws Exception {
-        // Confirm that when createNotificationChannels is called from the relevant app and not
-        // system, then it cannot set fields that can't be set by apps
-        mService.isSystemUid = false;
-
-        final NotificationChannel channel =
-                new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
-        channel.setBypassDnd(true);
-        channel.setAllowBubbles(true);
-
-        mBinderService.createNotificationChannels(PKG,
-                new ParceledListSlice(Arrays.asList(channel)));
-
-        final NotificationChannel createdChannel =
-                mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
-        assertFalse(createdChannel.canBypassDnd());
-        assertFalse(createdChannel.canBubble());
-    }
-
-    @Test
-    public void testCreateNotificationChannels_fromSystemCanSetFields() throws Exception {
-        // Confirm that when createNotificationChannels is called from system,
-        // then it can set fields that can't be set by apps
-        mService.isSystemUid = true;
-
-        final NotificationChannel channel =
-                new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
-        channel.setBypassDnd(true);
-        channel.setAllowBubbles(true);
-
-        mBinderService.createNotificationChannels(PKG,
-                new ParceledListSlice(Arrays.asList(channel)));
-
-        final NotificationChannel createdChannel =
-                mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
-        assertTrue(createdChannel.canBypassDnd());
-        assertTrue(createdChannel.canBubble());
-    }
-
-    @Test
     public void testBlockedNotifications_suspended() throws Exception {
         when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(true);
 
@@ -3132,8 +3088,6 @@
 
     @Test
     public void testDeleteChannelGroupChecksForFgses() throws Exception {
-        // the setup for this test requires it to seem like it's coming from the app
-        mService.isSystemUid = false;
         when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid)))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
         CountDownLatch latch = new CountDownLatch(2);
@@ -3146,7 +3100,7 @@
             ParceledListSlice<NotificationChannel> pls =
                     new ParceledListSlice(ImmutableList.of(notificationChannel));
             try {
-                mBinderService.createNotificationChannels(PKG, pls);
+                mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
             } catch (RemoteException e) {
                 throw new RuntimeException(e);
             }
@@ -3165,10 +3119,8 @@
                 ParceledListSlice<NotificationChannel> pls =
                         new ParceledListSlice(ImmutableList.of(notificationChannel));
                 try {
-                    // Because existing channels won't have their groups overwritten when the call
-                    // is from the app, this call won't take the channel out of the group
-                    mBinderService.createNotificationChannels(PKG, pls);
-                    mBinderService.deleteNotificationChannelGroup(PKG, "group");
+                mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
+                mBinderService.deleteNotificationChannelGroup(PKG, "group");
                 } catch (RemoteException e) {
                     throw new RuntimeException(e);
                 }
@@ -8673,7 +8625,7 @@
         assertEquals("friend", friendChannel.getConversationId());
         assertEquals(null, original.getConversationId());
         assertEquals(original.canShowBadge(), friendChannel.canShowBadge());
-        assertEquals(original.canBubble(), friendChannel.canBubble()); // called by system
+        assertFalse(friendChannel.canBubble()); // can't be modified by app
         assertFalse(original.getId().equals(friendChannel.getId()));
         assertNotNull(friendChannel.getId());
     }