Merge "Add data layer for the DeviceEntryIcon" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 44f3d70..52200bf 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -180,6 +180,18 @@
     srcs: ["core/java/android/nfc/*.aconfig"],
 }
 
+cc_aconfig_library {
+    name: "android_nfc_flags_aconfig_c_lib",
+    vendor_available: true,
+    aconfig_declarations: "android.nfc.flags-aconfig",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.nfcservices",
+        "nfc_nci.st21nfc.default",
+    ],
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 java_aconfig_library {
     name: "android.nfc.flags-aconfig-java",
     aconfig_declarations: "android.nfc.flags-aconfig",
diff --git a/core/api/current.txt b/core/api/current.txt
index 9ce5342..d80b5cb 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -28698,14 +28698,17 @@
   }
 
   public final class NfcAdapter {
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean allowTransaction();
     method public void disableForegroundDispatch(android.app.Activity);
     method public void disableReaderMode(android.app.Activity);
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean disallowTransaction();
     method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]);
     method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle);
     method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context);
     method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo();
     method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler);
     method public boolean isEnabled();
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeSupported();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionEnabled();
     method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionSupported();
     method public boolean isSecureNfcEnabled();
@@ -28813,6 +28816,7 @@
     method public boolean removeAidsForService(android.content.ComponentName, String);
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String);
     method public boolean setPreferredService(android.app.Activity, android.content.ComponentName);
+    method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setServiceObserveModeDefault(@NonNull android.content.ComponentName, boolean);
     method public boolean supportsAidPrefixRegistration();
     method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName);
     method public boolean unsetPreferredService(android.app.Activity);
@@ -28832,9 +28836,20 @@
     method public final android.os.IBinder onBind(android.content.Intent);
     method public abstract void onDeactivated(int);
     method public abstract byte[] processCommandApdu(byte[], android.os.Bundle);
+    method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void processPollingFrames(@NonNull java.util.List<android.os.Bundle>);
     method public final void sendResponseApdu(byte[]);
     field public static final int DEACTIVATION_DESELECTED = 1; // 0x1
     field public static final int DEACTIVATION_LINK_LOSS = 0; // 0x0
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_A = 65; // 0x0041 'A'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_B = 66; // 0x0042 'B'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_F = 70; // 0x0046 'F'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_OFF = 88; // 0x0058 'X'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_ON = 79; // 0x004f 'O'
+    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_UNKNOWN = 85; // 0x0055 'U'
     field public static final String SERVICE_INTERFACE = "android.nfc.cardemulation.action.HOST_APDU_SERVICE";
     field public static final String SERVICE_META_DATA = "android.nfc.cardemulation.host_apdu_service";
   }
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index aca6d06..5d06978 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -3402,8 +3402,8 @@
             new Key<Long>("android.sensor.exposureTime", long.class);
 
     /**
-     * <p>Duration from start of frame exposure to
-     * start of next frame exposure.</p>
+     * <p>Duration from start of frame readout to
+     * start of next frame readout.</p>
      * <p>The maximum frame rate that can be supported by a camera subsystem is
      * a function of many factors:</p>
      * <ul>
@@ -3464,6 +3464,10 @@
      * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
      * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * <p><em>Note:</em> Prior to Android 13, this field was described as measuring the duration from
+     * start of frame exposure to start of next frame exposure, which doesn't reflect the
+     * definition from sensor manufacturer. A mobile sensor defines the frame duration as
+     * intervals between sensor readouts.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }.
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1c66f82..0d204f3 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -4103,8 +4103,8 @@
             new Key<Long>("android.sensor.exposureTime", long.class);
 
     /**
-     * <p>Duration from start of frame exposure to
-     * start of next frame exposure.</p>
+     * <p>Duration from start of frame readout to
+     * start of next frame readout.</p>
      * <p>The maximum frame rate that can be supported by a camera subsystem is
      * a function of many factors:</p>
      * <ul>
@@ -4165,6 +4165,10 @@
      * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
      * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * <p><em>Note:</em> Prior to Android 13, this field was described as measuring the duration from
+     * start of frame exposure to start of next frame exposure, which doesn't reflect the
+     * definition from sensor manufacturer. A mobile sensor defines the frame duration as
+     * intervals between sensor readouts.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }.
diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl
index 0c95c2e..f6beec1 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -84,4 +84,6 @@
     boolean isReaderOptionSupported();
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)")
     boolean enableReaderOption(boolean enable);
+    boolean isObserveModeSupported();
+    boolean setObserveMode(boolean enabled);
 }
diff --git a/core/java/android/nfc/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl
index c7b3b2c..191385a 100644
--- a/core/java/android/nfc/INfcCardEmulation.aidl
+++ b/core/java/android/nfc/INfcCardEmulation.aidl
@@ -30,6 +30,7 @@
     boolean isDefaultServiceForAid(int userHandle, in ComponentName service, String aid);
     boolean setDefaultServiceForCategory(int userHandle, in ComponentName service, String category);
     boolean setDefaultForNextTap(int userHandle, in ComponentName service);
+    boolean setServiceObserveModeDefault(int userId, in android.content.ComponentName service, boolean enable);
     boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup);
     boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement);
     boolean unsetOffHostForService(int userHandle, in ComponentName service);
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index c897595..98a980f 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -1081,6 +1081,61 @@
         }
     }
 
+
+    /**
+     * Returns whether the device supports observer mode or not. When observe
+     * mode is enabled, the NFC hardware will listen for NFC readers, but not
+     * respond to them. When observe mode is disabled, the NFC hardware will
+     * resoond to the reader and proceed with the transaction.
+     * @return true if the mode is supported, false otherwise.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean isObserveModeSupported() {
+        try {
+            return sService.isObserveModeSupported();
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
+   /**
+    * Disables observe mode to allow the transaction to proceed. See
+    * {@link #isObserveModeSupported()} for a description of observe mode and
+    * use {@link #disallowTransaction()} to enable observe mode and block
+    * transactions again.
+    *
+    * @return boolean indicating success or failure.
+    */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean allowTransaction() {
+        try {
+            return sService.setObserveMode(false);
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
+    /**
+    * Signals that the transaction has completed and observe mode may be
+    * reenabled. See {@link #isObserveModeSupported()} for a description of
+    * observe mode and use {@link #allowTransaction()} to disable observe
+    * mode and allow transactions to proceed.
+    *
+    * @return boolean indicating success or failure.
+    */
+
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean disallowTransaction() {
+        try {
+            return sService.setObserveMode(true);
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            return false;
+        }
+    }
+
     /**
      * Resumes default polling for the current device state if polling is paused. Calling
      * this while polling is not paused is a no-op.
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index d048b59..58b6179 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -328,6 +328,24 @@
             return SELECTION_MODE_ASK_IF_CONFLICT;
         }
     }
+    /**
+     * Sets whether the system should default to observe mode or not when
+     * the service is in the foreground or the default payment service.
+     *
+     * @param service The component name of the service
+     * @param enable Whether the servic should default to observe mode or not
+     * @return whether the change was successful.
+     */
+    @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
+    public boolean setServiceObserveModeDefault(@NonNull ComponentName service, boolean enable) {
+        try {
+            return sService.setServiceObserveModeDefault(mContext.getUser().getIdentifier(),
+                    service, enable);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to reach CardEmulationService.");
+        }
+        return  false;
+    }
 
     /**
      * Registers a list of AIDs for a specific category for the
diff --git a/core/java/android/nfc/cardemulation/HostApduService.java b/core/java/android/nfc/cardemulation/HostApduService.java
index 55d0e73..7cd2533 100644
--- a/core/java/android/nfc/cardemulation/HostApduService.java
+++ b/core/java/android/nfc/cardemulation/HostApduService.java
@@ -16,11 +16,14 @@
 
 package android.nfc.cardemulation;
 
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.Service;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -29,6 +32,9 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * <p>HostApduService is a convenience {@link Service} class that can be
  * extended to emulate an NFC card inside an Android
@@ -230,9 +236,99 @@
     /**
      * @hide
      */
+    public static final int MSG_POLLING_LOOP = 4;
+
+    /**
+     * @hide
+     */
     public static final String KEY_DATA = "data";
 
     /**
+     * POLLING_LOOP_TYPE_KEY is the Bundle key for the type of
+     * polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE";
+
+    /**
+     * POLLING_LOOP_TYPE_A is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-A.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_A = 'A';
+
+    /**
+     * POLLING_LOOP_TYPE_B is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-B.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_B = 'B';
+
+    /**
+     * POLLING_LOOP_TYPE_F is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop is for NFC-F.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_F = 'F';
+
+    /**
+     * POLLING_LOOP_TYPE_ON is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop turns on.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_ON = 'O';
+
+    /**
+     * POLLING_LOOP_TYPE_OFF is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop turns off.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_OFF = 'X';
+
+    /**
+     * POLLING_LOOP_TYPE_UNKNOWN is the value associated with the key
+     * POLLING_LOOP_TYPE  in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the polling loop frame isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final char POLLING_LOOP_TYPE_UNKNOWN = 'U';
+
+    /**
+     * POLLING_LOOP_DATA is the Bundle key for the raw data of captured from
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA";
+
+    /**
+     * POLLING_LOOP_GAIN_KEY is the Bundle key for the field strength of
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN";
+
+    /**
+     * POLLING_LOOP_TIMESTAMP_KEY is the Bundle key for the timestamp of
+     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * when the frame type isn't recognized.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP";
+
+    /**
+     * @hide
+     */
+    public static final String POLLING_LOOP_FRAMES_BUNDLE_KEY =
+            "android.nfc.cardemulation.POLLING_FRAMES";
+
+    /**
      * Messenger interface to NfcService for sending responses.
      * Only accessed on main thread by the message handler.
      *
@@ -255,6 +351,7 @@
 
                 byte[] apdu = dataBundle.getByteArray(KEY_DATA);
                 if (apdu != null) {
+                        HostApduService has = HostApduService.this;
                     byte[] responseApdu = processCommandApdu(apdu, null);
                     if (responseApdu != null) {
                         if (mNfcService == null) {
@@ -306,6 +403,12 @@
                     Log.e(TAG, "RemoteException calling into NfcService.");
                 }
                 break;
+                case MSG_POLLING_LOOP:
+                    ArrayList<Bundle> frames =
+                            msg.getData().getParcelableArrayList(POLLING_LOOP_FRAMES_BUNDLE_KEY,
+                            Bundle.class);
+                    processPollingFrames(frames);
+                    break;
             default:
                 super.handleMessage(msg);
             }
@@ -366,6 +469,21 @@
         }
     }
 
+    /**
+     * This method is called when a polling frame has been received from a
+     * remote device. If the device is in observe mode, the service should
+     * call {@link NfcAdapter#allowTransaction()} once it is ready to proceed
+     * with the transaction. If the device is not in observe mode, the service
+     * can use this polling frame information to determine how to proceed if it
+     * subsequently has {@link #processCommandApdu(byte[], Bundle)} called. The
+     * service must override this method inorder to receive polling frames,
+     * otherwise the base implementation drops the frame.
+     *
+     * @param frame A description of the polling frame.
+     */
+    @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+    public void processPollingFrames(@NonNull List<Bundle> frame) {
+    }
 
     /**
      * <p>This method will be called when a command APDU has been received
diff --git a/core/java/android/nfc/flags.aconfig b/core/java/android/nfc/flags.aconfig
index cd50ace..17e0427 100644
--- a/core/java/android/nfc/flags.aconfig
+++ b/core/java/android/nfc/flags.aconfig
@@ -20,3 +20,31 @@
     description: "Flag for NFC user restriction"
     bug: "291187960"
 }
+
+flag {
+    name: "nfc_observe_mode"
+    namespace: "nfc"
+    description: "Enable NFC Observe Mode"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_read_polling_loop"
+    namespace: "nfc"
+    description: "Enable NFC Polling Loop Notifications"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_observe_mode_st_shim"
+    namespace: "nfc"
+    description: "Enable NFC Observe Mode ST shim"
+    bug: "294217286"
+}
+
+flag {
+    name: "nfc_read_polling_loop_st_shim"
+    namespace: "nfc"
+    description: "Enable NFC Polling Loop Notifications ST shim"
+    bug: "294217286"
+}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 0ad6c99..b600b22 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -35,4 +35,11 @@
     name: "fullscreen_dim_flag"
     description: "Whether to allow showing fullscreen dim on ActivityEmbedding split"
     bug: "253533308"
+}
+
+flag {
+    namespace: "windowing_sdk"
+    name: "activity_embedding_interactive_divider_flag"
+    description: "Whether the interactive divider feature is enabled"
+    bug: "293654166"
 }
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 3496994..698c5ba 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -4303,6 +4303,9 @@
         <!-- Whether the device must be screen on before routing data to this service.
              The default is true.-->
         <attr name="requireDeviceScreenOn" format="boolean"/>
+        <!-- Whether the device should default to observe mode when this service is
+             default or in the foreground. -->
+        <attr name="defaultToObserveMode" format="boolean"/>
     </declare-styleable>
 
     <!-- Use <code>offhost-apdu-service</code> as the root tag of the XML resource that
@@ -4327,6 +4330,9 @@
         <!-- Whether the device must be screen on before routing data to this service.
              The default is false.-->
         <attr name="requireDeviceScreenOn"/>
+        <!-- Whether the device should default to observe mode when this service is
+             default or in the foreground. -->
+        <attr name="defaultToObserveMode"/>
     </declare-styleable>
 
     <!-- Specify one or more <code>aid-group</code> elements inside a
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index a1b05c1..a7d6423 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -597,7 +597,13 @@
             SkIRect clipBounds;
             if (enableClip) {
                 uirenderer::Rect initialClipBounds;
-                props.getClippingRectForFlags(props.getClippingFlags(), &initialClipBounds);
+                const auto clipFlags = props.getClippingFlags();
+                if (clipFlags) {
+                    props.getClippingRectForFlags(clipFlags, &initialClipBounds);
+                } else {
+                    // Works for RenderNode::damageSelf()
+                    initialClipBounds.set(DIRTY_MIN, DIRTY_MIN, DIRTY_MAX, DIRTY_MAX);
+                }
                 clipBounds =
                         info.damageAccumulator
                                 ->computeClipAndTransform(initialClipBounds.toSkRect(), &transform)
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
new file mode 100644
index 0000000..92cc923
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+/**
+ * Interface to the TV AD service.
+ * @hide
+ */
+interface ITvAdManager {
+    void startAdService(in IBinder sessionToken, int userId);
+}
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
new file mode 100644
index 0000000..b834f1b9
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+/**
+ * Sub-interface of ITvAdService which is created per session and has its own context.
+ * @hide
+ */
+oneway interface ITvAdSession {
+    void startAdService();
+}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
new file mode 100644
index 0000000..aa5a290
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Central system API to the overall client-side TV AD architecture, which arbitrates interaction
+ * between applications and AD services.
+ * @hide
+ */
+public class TvAdManager {
+    private static final String TAG = "TvAdManager";
+
+    private final ITvAdManager mService;
+    private final int mUserId;
+
+    public TvAdManager(ITvAdManager service, int userId) {
+        mService = service;
+        mUserId = userId;
+    }
+
+    /**
+     * The Session provides the per-session functionality of AD service.
+     */
+    public static final class Session {
+        private final IBinder mToken;
+        private final ITvAdManager mService;
+        private final int mUserId;
+
+        private Session(IBinder token, ITvAdManager service, int userId) {
+            mToken = token;
+            mService = service;
+            mUserId = userId;
+        }
+
+        void startAdService() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.startAdService(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
new file mode 100644
index 0000000..61101f0
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.app.Service;
+import android.view.KeyEvent;
+
+/**
+ * The TvAdService class represents a TV client-side advertisement service.
+ * @hide
+ */
+public abstract class TvAdService extends Service {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvAdService";
+
+    /**
+     * Base class for derived classes to implement to provide a TV AD session.
+     */
+    public abstract static class Session implements KeyEvent.Callback {
+        /**
+         * Starts TvAdService session.
+         */
+        public void onStartAdService() {
+        }
+
+        void startAdService() {
+            onStartAdService();
+        }
+    }
+
+    /**
+     * Implements the internal ITvAdService interface.
+     */
+    public static class ITvAdSessionWrapper extends ITvAdSession.Stub {
+        private final Session mSessionImpl;
+
+        public ITvAdSessionWrapper(Session mSessionImpl) {
+            this.mSessionImpl = mSessionImpl;
+        }
+
+        @Override
+        public void startAdService() {
+            mSessionImpl.startAdService();
+        }
+    }
+}
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
new file mode 100644
index 0000000..1a3771a
--- /dev/null
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.ad;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.ViewGroup;
+
+/**
+ * Displays contents of TV AD services.
+ * @hide
+ */
+public class TvAdView extends ViewGroup {
+    private static final String TAG = "TvAdView";
+    private static final boolean DEBUG = false;
+
+    // TODO: create session
+    private TvAdManager.Session mSession;
+
+    public TvAdView(Context context) {
+        super(context, /* attrs = */null, /* defStyleAttr = */0);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (DEBUG) {
+            Log.d(TAG,
+                    "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+        }
+    }
+
+    /**
+     * Starts the AD service.
+     */
+    public void startAdService() {
+        if (DEBUG) {
+            Log.d(TAG, "start");
+        }
+        if (mSession != null) {
+            mSession.startAdService();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 12a083e..5e19439 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -116,7 +116,7 @@
             )
 
     override fun forceUpdate() {
-        forceUpdates.tryEmit(Unit)
+        tileScope.launch { forceUpdates.emit(Unit) }
     }
 
     override fun onUserChanged(user: UserHandle) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 84d2b37..404621d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,7 +34,6 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
@@ -83,7 +82,6 @@
 import android.util.SparseBooleanArray;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
@@ -120,7 +118,6 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.Prefs;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -304,7 +301,6 @@
     private final DevicePostureController mDevicePostureController;
     private @DevicePostureController.DevicePostureInt int mDevicePosture;
     private int mOrientation;
-    private final FeatureFlags mFeatureFlags;
     private final Lazy<SecureSettings> mSecureSettings;
     private int mDialogTimeoutMillis;
 
@@ -323,9 +319,7 @@
             DevicePostureController devicePostureController,
             Looper looper,
             DumpManager dumpManager,
-            FeatureFlags featureFlags,
             Lazy<SecureSettings> secureSettings) {
-        mFeatureFlags = featureFlags;
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mHandler = new H(looper);
@@ -1373,14 +1367,12 @@
 
     private void provideTouchFeedbackH(int newRingerMode) {
         VibrationEffect effect = null;
-        int hapticConstant = HapticFeedbackConstants.NO_HAPTICS;
         switch (newRingerMode) {
             case RINGER_MODE_NORMAL:
                 mController.scheduleTouchFeedback();
                 break;
             case RINGER_MODE_SILENT:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-                hapticConstant = HapticFeedbackConstants.TOGGLE_OFF;
                 break;
             case RINGER_MODE_VIBRATE:
                 // Feedback handled by onStateChange, for feedback both when user toggles
@@ -1388,11 +1380,8 @@
                 break;
             default:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
-                hapticConstant = HapticFeedbackConstants.TOGGLE_ON;
         }
-        if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-            mDialogView.performHapticFeedback(hapticConstant);
-        } else if (effect != null) {
+        if (effect != null) {
             mController.vibrate(effect);
         }
     }
@@ -1820,22 +1809,7 @@
                 && mState.ringerModeInternal != -1
                 && mState.ringerModeInternal != state.ringerModeInternal
                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
-
-            if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-                if (mShowing) {
-                    // The dialog view is responsible for triggering haptics in the oneway API
-                    mDialogView.performHapticFeedback(HapticFeedbackConstants.TOGGLE_ON);
-                }
-                /*
-                TODO(b/290642122): If the dialog is not showing, we have the case where haptics is
-                enabled by dragging the volume slider of Settings to a value of 0. This must be
-                handled by view Slices in Settings whilst using the performHapticFeedback API.
-                 */
-
-            } else {
-                // Old behavior only active if the oneway API is not used.
-                mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
-            }
+            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
         }
         mState = state;
         mDynamic.clear();
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index e3b3c21..53217d4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -22,7 +22,6 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -65,7 +64,6 @@
             CsdWarningDialog.Factory csdFactory,
             DevicePostureController devicePostureController,
             DumpManager dumpManager,
-            FeatureFlags featureFlags,
             Lazy<SecureSettings> secureSettings) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
@@ -82,7 +80,6 @@
                 devicePostureController,
                 Looper.getMainLooper(),
                 dumpManager,
-                featureFlags,
                 secureSettings);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
deleted file mode 100644
index d3b7daa..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.viewmodel
-
-import android.os.UserHandle
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
-import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
-import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
-import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
-import com.android.systemui.qs.tiles.base.logging.QSTileLogger
-import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-// TODO(b/299909368): Add more tests
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() {
-
-    @Mock private lateinit var qsTileLogger: QSTileLogger
-    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
-
-    private val fakeUserRepository = FakeUserRepository()
-    private val fakeQSTileDataInteractor = FakeQSTileDataInteractor<Any>()
-    private val fakeQSTileUserActionInteractor = FakeQSTileUserActionInteractor<Any>()
-    private val fakeDisabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
-    private val fakeFalsingManager = FalsingManagerFake()
-
-    private val testCoroutineDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testCoroutineDispatcher)
-
-    private lateinit var underTest: QSTileViewModel
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        underTest = createViewModel(testScope)
-    }
-
-    @Test
-    fun testDoesntListenStateUntilCreated() =
-        testScope.runTest {
-            assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
-
-            assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
-
-            underTest.state.launchIn(backgroundScope)
-            runCurrent()
-
-            assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty()
-            assertThat(fakeQSTileDataInteractor.dataRequests.first())
-                .isEqualTo(FakeQSTileDataInteractor.DataRequest(UserHandle.of(0)))
-        }
-
-    private fun createViewModel(
-        scope: TestScope,
-        config: QSTileConfig = TEST_QS_TILE_CONFIG,
-    ): QSTileViewModel =
-        QSTileViewModelImpl(
-            config,
-            { fakeQSTileUserActionInteractor },
-            { fakeQSTileDataInteractor },
-            {
-                object : QSTileDataToStateMapper<Any> {
-                    override fun map(config: QSTileConfig, data: Any): QSTileState =
-                        QSTileState.build(
-                            { Icon.Resource(0, ContentDescription.Resource(0)) },
-                            ""
-                        ) {}
-                }
-            },
-            fakeDisabledByPolicyInteractor,
-            fakeUserRepository,
-            fakeFalsingManager,
-            qsTileAnalytics,
-            qsTileLogger,
-            FakeSystemClock(),
-            testCoroutineDispatcher,
-            scope.backgroundScope,
-        )
-
-    private companion object {
-
-        val TEST_QS_TILE_CONFIG = QSTileConfigTestBuilder.build {}
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
new file mode 100644
index 0000000..3a0ebdb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.viewmodel
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSTileViewModelTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsTileLogger: QSTileLogger
+    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
+
+    private val tileConfig =
+        QSTileConfigTestBuilder.build { policy = QSTilePolicy.Restricted("test_restriction") }
+
+    private val userRepository = FakeUserRepository()
+    private val tileDataInteractor = FakeQSTileDataInteractor<String>()
+    private val tileUserActionInteractor = FakeQSTileUserActionInteractor<String>()
+    private val disabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
+    private val falsingManager = FalsingManagerFake()
+
+    private val testCoroutineDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testCoroutineDispatcher)
+
+    private lateinit var underTest: QSTileViewModel
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = createViewModel(testScope)
+    }
+
+    @Test
+    fun stateReceivedForTheData() =
+        testScope.runTest {
+            val testTileData = "test_tile_data"
+            val states = collectValues(underTest.state)
+            runCurrent()
+
+            tileDataInteractor.emitData(testTileData)
+            runCurrent()
+
+            assertThat(states()).isNotEmpty()
+            assertThat(states().first().label).isEqualTo(testTileData)
+            verify(qsTileLogger).logInitialRequest(eq(tileConfig.tileSpec))
+        }
+
+    @Test
+    fun doesntListenDataIfStateIsntListened() =
+        testScope.runTest {
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(0)
+
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(1)
+        }
+
+    @Test
+    fun doesntListenAvailabilityIfAvailabilityIsntListened() =
+        testScope.runTest {
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(0)
+
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(1)
+        }
+
+    @Test
+    fun doesntListedDataAfterDestroy() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.destroy()
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataSubscriptionCount.value).isEqualTo(0)
+            assertThat(tileDataInteractor.availabilitySubscriptionCount.value).isEqualTo(0)
+        }
+
+    @Test
+    fun forceUpdateTriggersData() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.forceUpdate()
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isInstanceOf(DataUpdateTrigger.ForceUpdate::class.java)
+            verify(qsTileLogger).logForceUpdate(eq(tileConfig.tileSpec))
+        }
+
+    @Test
+    fun userChangeUpdatesData() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onUserChanged(USER)
+            runCurrent()
+
+            assertThat(tileDataInteractor.dataRequests.last())
+                .isEqualTo(FakeQSTileDataInteractor.DataRequest(USER))
+        }
+
+    @Test
+    fun userChangeUpdatesAvailability() =
+        testScope.runTest {
+            underTest.isAvailable.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onUserChanged(USER)
+            runCurrent()
+
+            assertThat(tileDataInteractor.availabilityRequests.last())
+                .isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
+        }
+
+    private fun createViewModel(
+        scope: TestScope,
+        config: QSTileConfig = tileConfig,
+    ): QSTileViewModel =
+        QSTileViewModelImpl(
+            config,
+            { tileUserActionInteractor },
+            { tileDataInteractor },
+            {
+                object : QSTileDataToStateMapper<String> {
+                    override fun map(config: QSTileConfig, data: String): QSTileState =
+                        QSTileState.build(
+                            { Icon.Resource(0, ContentDescription.Resource(0)) },
+                            data
+                        ) {}
+                }
+            },
+            disabledByPolicyInteractor,
+            userRepository,
+            falsingManager,
+            qsTileAnalytics,
+            qsTileLogger,
+            FakeSystemClock(),
+            testCoroutineDispatcher,
+            scope.backgroundScope,
+        )
+
+    private companion object {
+
+        val USER = UserHandle.of(1)!!
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
new file mode 100644
index 0000000..ea8acc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.viewmodel
+
+import androidx.test.filters.MediumTest
+import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** Tests all possible [QSTileUserAction]s. If you need */
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSTileViewModelUserInputTest : SysuiTestCase() {
+
+    @Mock private lateinit var qsTileLogger: QSTileLogger
+    @Mock private lateinit var qsTileAnalytics: QSTileAnalytics
+
+    @Parameter lateinit var userAction: QSTileUserAction
+
+    private val tileConfig =
+        QSTileConfigTestBuilder.build { policy = QSTilePolicy.Restricted("test_restriction") }
+
+    private val userRepository = FakeUserRepository()
+    private val tileDataInteractor = FakeQSTileDataInteractor<String>()
+    private val tileUserActionInteractor = FakeQSTileUserActionInteractor<String>()
+    private val disabledByPolicyInteractor = FakeDisabledByPolicyInteractor()
+    private val falsingManager = FalsingManagerFake()
+
+    private val testCoroutineDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testCoroutineDispatcher)
+
+    private lateinit var underTest: QSTileViewModel
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = createViewModel(testScope)
+    }
+
+    @Test
+    fun userInputTriggersData() =
+        testScope.runTest {
+            tileDataInteractor.emitData("initial_data")
+            underTest.state.launchIn(backgroundScope)
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserAction(eq(userAction), eq(tileConfig.tileSpec), eq(true), eq(true))
+            verify(qsTileLogger)
+                .logUserActionPipeline(
+                    eq(tileConfig.tileSpec),
+                    eq(userAction),
+                    any(),
+                    eq("initial_data")
+                )
+            verify(qsTileAnalytics).trackUserAction(eq(tileConfig), eq(userAction))
+        }
+
+    @Test
+    fun disabledByPolicyUserInputIsSkipped() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            disabledByPolicyInteractor.policyResult =
+                DisabledByPolicyInteractor.PolicyResult.TileDisabled(
+                    RestrictedLockUtils.EnforcedAdmin()
+                )
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isNotInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserActionRejectedByPolicy(eq(userAction), eq(tileConfig.tileSpec))
+            verify(qsTileAnalytics, never()).trackUserAction(any(), any())
+        }
+
+    @Test
+    fun falsedUserInputIsSkipped() =
+        testScope.runTest {
+            underTest.state.launchIn(backgroundScope)
+            falsingManager.setFalseLongTap(true)
+            falsingManager.setFalseTap(true)
+            runCurrent()
+
+            underTest.onActionPerformed(userAction)
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.last())
+                .isNotInstanceOf(DataUpdateTrigger.UserInput::class.java)
+            verify(qsTileLogger)
+                .logUserActionRejectedByFalsing(eq(userAction), eq(tileConfig.tileSpec))
+            verify(qsTileAnalytics, never()).trackUserAction(any(), any())
+        }
+
+    @Test
+    fun userInputIsThrottled() =
+        testScope.runTest {
+            val inputCount = 100
+            underTest.state.launchIn(backgroundScope)
+
+            repeat(inputCount) { underTest.onActionPerformed(userAction) }
+            runCurrent()
+
+            assertThat(tileDataInteractor.triggers.size).isLessThan(inputCount)
+        }
+
+    private fun createViewModel(scope: TestScope): QSTileViewModel =
+        QSTileViewModelImpl(
+            tileConfig,
+            { tileUserActionInteractor },
+            { tileDataInteractor },
+            {
+                object : QSTileDataToStateMapper<String> {
+                    override fun map(config: QSTileConfig, data: String): QSTileState =
+                        QSTileState.build(
+                            { Icon.Resource(0, ContentDescription.Resource(0)) },
+                            data
+                        ) {}
+                }
+            },
+            disabledByPolicyInteractor,
+            userRepository,
+            falsingManager,
+            qsTileAnalytics,
+            qsTileLogger,
+            FakeSystemClock(),
+            testCoroutineDispatcher,
+            scope.backgroundScope,
+        )
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters
+        fun data(): Iterable<QSTileUserAction> =
+            listOf(
+                QSTileUserAction.Click(null),
+                QSTileUserAction.LongClick(null),
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 7456e00..8c823b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -20,7 +20,6 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
@@ -68,7 +67,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialogController;
@@ -149,14 +147,13 @@
         }
     };
 
-    private FakeFeatureFlags mFeatureFlags;
     private int mLongestHideShowAnimationDuration = 250;
     private FakeSettings mSecureSettings;
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
 
-    @Before
+   @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
@@ -179,8 +176,6 @@
 
         mConfigurationController = new FakeConfigurationController();
 
-        mFeatureFlags = new FakeFeatureFlags();
-
         mSecureSettings = new FakeSettings();
 
         when(mLazySecureSettings.get()).thenReturn(mSecureSettings);
@@ -200,7 +195,6 @@
                 mPostureController,
                 mTestableLooper.getLooper(),
                 mDumpManager,
-                mFeatureFlags,
                 mLazySecureSettings);
         mDialog.init(0, null);
         State state = createShellState();
@@ -328,7 +322,6 @@
 
     @Test
     public void testVibrateOnRingerChangedToVibrate() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialSilentState = new State();
         initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
 
@@ -349,30 +342,7 @@
     }
 
     @Test
-    public void testControllerDoesNotVibrateOnRingerChangedToVibrate_OnewayAPI_On() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialSilentState = new State();
-        initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT;
-
-        final State vibrateState = new State();
-        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-
-        // change ringer to silent
-        mDialog.onStateChangedH(initialSilentState);
-
-        // expected: shouldn't call vibrate yet
-        verify(mVolumeDialogController, never()).vibrate(any());
-
-        // changed ringer to vibrate
-        mDialog.onStateChangedH(vibrateState);
-
-        // expected: vibrate method of controller is not used
-        verify(mVolumeDialogController, never()).vibrate(any());
-    }
-
-    @Test
     public void testNoVibrateOnRingerInitialization() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = -1;
 
@@ -390,29 +360,9 @@
     }
 
     @Test
-    public void testControllerDoesNotVibrateOnRingerInitialization_OnewayAPI_On() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = -1;
-
-        // ringer not initialized yet:
-        mDialog.onStateChangedH(initialUnsetState);
-
-        final State vibrateState = new State();
-        vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-
-        // changed ringer to vibrate
-        mDialog.onStateChangedH(vibrateState);
-
-        // shouldn't call vibrate on the controller either
-        verify(mVolumeDialogController, never()).vibrate(any());
-    }
-
-    @Test
     public void testSelectVibrateFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -426,27 +376,9 @@
     }
 
     @Test
-    public void testSelectVibrateFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerVibrate.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                AudioManager.RINGER_MODE_VIBRATE, false);
-    }
-
-    @Test
     public void testSelectMuteFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
         mDialog.onStateChangedH(initialUnsetState);
@@ -460,27 +392,9 @@
     }
 
     @Test
-    public void testSelectMuteFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerMute.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                AudioManager.RINGER_MODE_SILENT, false);
-    }
-
-    @Test
     public void testSelectNormalFromDrawer() {
         assumeHasDrawer();
 
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
         mDialog.onStateChangedH(initialUnsetState);
@@ -493,23 +407,6 @@
                 AudioManager.RINGER_MODE_NORMAL, false);
     }
 
-    @Test
-    public void testSelectNormalFromDrawer_OnewayAPI_On() {
-        assumeHasDrawer();
-
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-        final State initialUnsetState = new State();
-        initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
-        mDialog.onStateChangedH(initialUnsetState);
-
-        mActiveRinger.performClick();
-        mDrawerNormal.performClick();
-
-        // Make sure we've actually changed the ringer mode.
-        verify(mVolumeDialogController, times(1)).setRingerMode(
-                RINGER_MODE_NORMAL, false);
-    }
-
     /**
      * Ideally we would look at the ringer ImageView and check its assigned drawable id, but that
      * API does not exist. So we do the next best thing; we check the cached icon id.
@@ -682,7 +579,6 @@
 
         State state = createShellState();
         state.ringerModeInternal = ringerMode;
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         mDialog.onStateChangedH(state);
 
         mDialog.show(SHOW_REASON_UNKNOWN);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
index 1efa74b..62765d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
@@ -20,7 +20,6 @@
 
 class FakeDisabledByPolicyInteractor : DisabledByPolicyInteractor {
 
-    var handleResult: Boolean = false
     var policyResult: DisabledByPolicyInteractor.PolicyResult =
         DisabledByPolicyInteractor.PolicyResult.TileEnabled
 
@@ -31,5 +30,9 @@
 
     override fun handlePolicyResult(
         policyResult: DisabledByPolicyInteractor.PolicyResult
-    ): Boolean = handleResult
+    ): Boolean =
+        when (policyResult) {
+            is DisabledByPolicyInteractor.PolicyResult.TileEnabled -> false
+            is DisabledByPolicyInteractor.PolicyResult.TileDisabled -> true
+        }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
index 2b3330f..3fcf8a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
@@ -17,16 +17,21 @@
 package com.android.systemui.qs.tiles.base.interactor
 
 import android.os.UserHandle
-import javax.annotation.CheckReturnValue
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.flatMapLatest
 
-class FakeQSTileDataInteractor<T>(
-    private val dataFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = Int.MAX_VALUE),
-    private val availabilityFlow: MutableSharedFlow<Boolean> =
-        MutableSharedFlow(replay = Int.MAX_VALUE),
-) : QSTileDataInteractor<T> {
+class FakeQSTileDataInteractor<T> : QSTileDataInteractor<T> {
+
+    private val dataFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = 1)
+    val dataSubscriptionCount
+        get() = dataFlow.subscriptionCount
+    private val availabilityFlow: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
+    val availabilitySubscriptionCount
+        get() = availabilityFlow.subscriptionCount
+
+    private val mutableTriggers = mutableListOf<DataUpdateTrigger>()
+    val triggers: List<DataUpdateTrigger> = mutableTriggers
 
     private val mutableDataRequests = mutableListOf<DataRequest>()
     val dataRequests: List<DataRequest> = mutableDataRequests
@@ -34,14 +39,17 @@
     private val mutableAvailabilityRequests = mutableListOf<AvailabilityRequest>()
     val availabilityRequests: List<AvailabilityRequest> = mutableAvailabilityRequests
 
-    @CheckReturnValue fun emitData(data: T): Boolean = dataFlow.tryEmit(data)
+    suspend fun emitData(data: T): Unit = dataFlow.emit(data)
 
     fun tryEmitAvailability(isAvailable: Boolean): Boolean = availabilityFlow.tryEmit(isAvailable)
     suspend fun emitAvailability(isAvailable: Boolean) = availabilityFlow.emit(isAvailable)
 
     override fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<T> {
         mutableDataRequests.add(DataRequest(user))
-        return triggers.flatMapLatest { dataFlow }
+        return triggers.flatMapLatest {
+            mutableTriggers.add(it)
+            dataFlow
+        }
     }
 
     override fun availability(user: UserHandle): Flow<Boolean> {
diff --git a/ravenwood/README-ravenwood+mockito.md b/ravenwood/README-ravenwood+mockito.md
new file mode 100644
index 0000000..6adb6144
--- /dev/null
+++ b/ravenwood/README-ravenwood+mockito.md
@@ -0,0 +1,24 @@
+# Ravenwood and Mockito
+
+Last update: 2023-11-13
+
+- As of 2023-11-13, `external/mockito` is based on version 2.x.
+- Mockito didn't support static mocking before 3.4.0.
+  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
+
+- Latest Mockito is 5.*. According to https://github.com/mockito/mockito:
+  `Mockito 3 does not introduce any breaking API changes, but now requires Java 8 over Java 6 for Mockito 2. Mockito 4 removes deprecated API. Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.`
+
+- Mockito now supports Android natively.
+  See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.1
+  - But it's unclear at this point to omakoto@ how the `mockito-android` module is built.
+
+- Potential plan:
+  - Ideal option:
+    - If we can update `external/mockito`, that'd be great, but it may not work because
+      Mockito has removed the deprecated APIs.
+  - Second option:
+    - Import the latest mockito as `external/mockito-new`, and require ravenwood
+      to use this one.
+    - The latest mockito needs be exposed to all of 1) device tests, 2) host tests, and 3) ravenwood tests.
+    - This probably will require the latest `bytebuddy` and `objenesis`.
\ No newline at end of file
diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp
index 6dbff4c..4135022 100644
--- a/ravenwood/mockito/Android.bp
+++ b/ravenwood/mockito/Android.bp
@@ -36,3 +36,37 @@
     ],
     auto_gen_config: true,
 }
+
+android_test {
+    name: "RavenwoodMockitoTest_device",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    static_libs: [
+        "junit",
+        "truth",
+
+        "androidx.test.rules",
+
+        "ravenwood-junit",
+
+        "mockito-target-extended-minus-junit4",
+    ],
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+    jni_libs: [
+        // Required by mockito
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/ravenwood/mockito/AndroidManifest.xml b/ravenwood/mockito/AndroidManifest.xml
new file mode 100644
index 0000000..15f0a29
--- /dev/null
+++ b/ravenwood/mockito/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.mockitotest">
+
+    <application android:debuggable="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.ravenwood.mockitotest"
+        />
+</manifest>
diff --git a/ravenwood/mockito/AndroidTest.xml b/ravenwood/mockito/AndroidTest.xml
new file mode 100644
index 0000000..96bc275
--- /dev/null
+++ b/ravenwood/mockito/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Frameworks Services Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="RavenwoodMockitoTest_device.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="FrameworksMockingServicesTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.ravenwood.mockitotest" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/MockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/MockitoTest.java
deleted file mode 100644
index b175ae7..0000000
--- a/ravenwood/mockito/test/com/android/ravenwood/mockito/MockitoTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwood.mockito;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.Intent;
-
-import org.junit.Test;
-
-public class MockitoTest {
-    @Test
-    public void testMockJdkClass() {
-        Process object = mock(Process.class);
-
-        when(object.exitValue()).thenReturn(42);
-
-        assertThat(object.exitValue()).isEqualTo(42);
-    }
-
-    /* It still doesn't work...
-STACKTRACE:
-org.mockito.exceptions.base.MockitoException:
-Mockito cannot mock this class: class android.content.Intent.
-
-Mockito can only mock non-private & non-final classes.
-If you're not sure why you're getting this error, please report to the mailing list.
-
-
-... But Intent public, non-final.
-
-     */
-    // @Test
-    private void testMockAndroidClass1() {
-        Intent object = mock(Intent.class);
-
-        when(object.getAction()).thenReturn("ACTION_RAVENWOOD");
-
-        assertThat(object.getAction()).isEqualTo("ACTION_RAVENWOOD");
-    }
-
-    @Test
-    public void testMockAndroidClass2() {
-        Context object = mock(Context.class);
-
-        when(object.getPackageName()).thenReturn("android");
-
-        assertThat(object.getPackageName()).isEqualTo("android");
-    }
-}
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
new file mode 100644
index 0000000..36fa3dd
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+public class RavenwoodMockitoTest {
+    @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+
+// Use this to mock static methods, which isn't supported by mockito 2.
+// Mockito supports static mocking since 3.4.0:
+// See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
+
+//    private MockitoSession mMockingSession;
+//
+//    @Before
+//    public void setUp() {
+//        mMockingSession = mockitoSession()
+//                .strictness(Strictness.LENIENT)
+//                .mockStatic(RavenwoodMockitoTest.class)
+//                .startMocking();
+//    }
+//
+//    @After
+//    public void tearDown() {
+//        if (mMockingSession != null) {
+//            mMockingSession.finishMocking();
+//        }
+//    }
+
+    @Test
+    public void testMockJdkClass() {
+        Process object = mock(Process.class);
+
+        when(object.exitValue()).thenReturn(42);
+
+        assertThat(object.exitValue()).isEqualTo(42);
+    }
+
+    /*
+ - Intent can't be mocked because of the dependency to `org.xmlpull.v1.XmlPullParser`.
+   (The error says "Mockito can only mock non-private & non-final classes", but that's likely a
+   red-herring.)
+
+STACKTRACE:
+org.mockito.exceptions.base.MockitoException:
+Mockito cannot mock this class: class android.content.Intent.
+
+  :
+
+Underlying exception : java.lang.IllegalArgumentException: Could not create type
+    at com.android.ravenwood.mockito.RavenwoodMockitoTest.testMockAndroidClass1
+    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
+
+  :
+
+Caused by: java.lang.ClassNotFoundException: org.xmlpull.v1.XmlPullParser
+    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
+    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
+    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
+    ... 54 more
+     */
+    @Test
+    @IgnoreUnderRavenwood
+    public void testMockAndroidClass1() {
+        Intent object = mock(Intent.class);
+
+        when(object.getAction()).thenReturn("ACTION_RAVENWOOD");
+
+        assertThat(object.getAction()).isEqualTo("ACTION_RAVENWOOD");
+    }
+
+    @Test
+    public void testMockAndroidClass2() {
+        Context object = mock(Context.class);
+
+        when(object.getPackageName()).thenReturn("android");
+
+        assertThat(object.getPackageName()).isEqualTo("android");
+    }
+}
diff --git a/services/proguard.flags b/services/proguard.flags
index 261bb7c..407505d 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -14,13 +14,20 @@
 }
 
 # APIs referenced by dependent JAR files and modules
--keep @interface android.annotation.SystemApi
+# TODO(b/300514883): Pull @SystemApi keep rules from system-api.pro.
+-keep interface android.annotation.SystemApi
 -keep @android.annotation.SystemApi class * {
   public protected *;
 }
 -keepclasseswithmembers class * {
   @android.annotation.SystemApi *;
 }
+# Also ensure nested classes are kept. This is overly conservative, but handles
+# cases where such classes aren't explicitly marked @SystemApi.
+-if @android.annotation.SystemApi class *
+-keep public class <1>$** {
+  public protected *;
+}
 
 # Derivatives of SystemService and other services created via reflection
 -keep,allowoptimization,allowaccessmodification class * extends com.android.server.SystemService {