Merge "Add StorageStatsManagerTest to POSTSUBMIT" into main
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 6b4340a..15a4715 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -38,6 +38,7 @@
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -55,7 +56,6 @@
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -146,7 +146,7 @@
                 forceConsumingTypes |= type;
             }
 
-            if (Flags.enableCaptionCompatInsetForceConsumptionAlways()
+            if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isEnabled()
                     && (flags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) {
                 forceConsumingOpaqueCaptionBar = true;
             }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9ff5031..33e7905 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -125,11 +125,11 @@
 import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
-import static com.android.window.flags.Flags.enableCaptionCompatInsetForceConsumption;
 import static com.android.window.flags.Flags.insetsControlChangedItem;
 import static com.android.window.flags.Flags.insetsControlSeq;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
@@ -3214,10 +3214,10 @@
             typesToShow |= Type.navigationBars();
         }
         if (captionIsHiddenByFlags && !captionWasHiddenByFlags
-                && enableCaptionCompatInsetForceConsumption()) {
+                && ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isEnabled()) {
             typesToHide |= Type.captionBar();
         } else if (!captionIsHiddenByFlags && captionWasHiddenByFlags
-                && enableCaptionCompatInsetForceConsumption()) {
+                && ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isEnabled()) {
             typesToShow |= Type.captionBar();
         }
         if (typesToHide != 0) {
diff --git a/core/java/android/window/flags/DesktopModeFlags.java b/core/java/android/window/flags/DesktopModeFlags.java
index 5c53d66..701b6be 100644
--- a/core/java/android/window/flags/DesktopModeFlags.java
+++ b/core/java/android/window/flags/DesktopModeFlags.java
@@ -17,7 +17,9 @@
 package android.window.flags;
 
 import android.annotation.Nullable;
-import android.content.Context;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.content.ContentResolver;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -39,9 +41,13 @@
  */
 public enum DesktopModeFlags {
     // All desktop mode related flags to be overridden by developer option toggle will be added here
-    DESKTOP_WINDOWING_MODE(
+    ENABLE_DESKTOP_WINDOWING_MODE(
             Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
-    DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, false);
+    ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, false),
+    ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION(
+            Flags::enableCaptionCompatInsetForceConsumption, true),
+    ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
+            Flags::enableCaptionCompatInsetForceConsumptionAlways, true);
 
     private static final String TAG = "DesktopModeFlagsUtil";
     // Function called to obtain aconfig flag value.
@@ -62,14 +68,15 @@
      * Determines state of flag based on the actual flag and desktop mode developer option
      * overrides.
      */
-    public boolean isEnabled(Context context) {
+    public boolean isEnabled() {
+        Application application = ActivityThread.currentApplication();
         if (!Flags.showDesktopWindowingDevOption()
                 || !mShouldOverrideByDevOption
-                || context.getContentResolver() == null) {
+                || application == null) {
             return mFlagFunction.get();
         } else {
             boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
-            return switch (getToggleOverride(context)) {
+            return switch (getToggleOverride(application.getContentResolver())) {
                 case OVERRIDE_UNSET -> mFlagFunction.get();
                 // When toggle override matches its default state, don't override flags. This
                 // helps users reset their feature overrides.
@@ -79,14 +86,14 @@
         }
     }
 
-    private ToggleOverride getToggleOverride(Context context) {
+    private ToggleOverride getToggleOverride(ContentResolver contentResolver) {
         // If cached, return it
         if (sCachedToggleOverride != null) {
             return sCachedToggleOverride;
         }
 
         // Otherwise, fetch and cache it
-        ToggleOverride override = getToggleOverrideFromSystem(context);
+        ToggleOverride override = getToggleOverrideFromSystem(contentResolver);
         sCachedToggleOverride = override;
         Log.d(TAG, "Toggle override initialized to: " + override);
         return override;
@@ -95,9 +102,9 @@
     /**
      *  Returns {@link ToggleOverride} from Settings.Global set by toggle.
      */
-    private ToggleOverride getToggleOverrideFromSystem(Context context) {
+    private ToggleOverride getToggleOverrideFromSystem(ContentResolver contentResolver) {
         int settingValue = Settings.Global.getInt(
-                context.getContentResolver(),
+                contentResolver,
                 Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
                 ToggleOverride.OVERRIDE_UNSET.getSetting()
         );
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 4708be8..84dfc49 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -38,6 +38,8 @@
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.flags.Flags.customizableWindowHeaders;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
 
 import static com.android.internal.policy.PhoneWindow.FEATURE_OPTIONS_PANEL;
 
@@ -114,7 +116,6 @@
 import com.android.internal.widget.ActionBarContextView;
 import com.android.internal.widget.BackgroundFallback;
 import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
-import com.android.window.flags.Flags;
 
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -1217,14 +1218,15 @@
 
         final boolean hideCaptionBar = fullscreen
                 || (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0;
-        final boolean consumingCaptionBar = Flags.enableCaptionCompatInsetForceConsumption()
-                && ((mLastForceConsumingTypes & WindowInsets.Type.captionBar()) != 0
+        final boolean consumingCaptionBar =
+                ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isEnabled()
+                        && ((mLastForceConsumingTypes & WindowInsets.Type.captionBar()) != 0
                         && hideCaptionBar);
 
         final boolean isOpaqueCaptionBar = customizableWindowHeaders()
                 && (appearance & APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) == 0;
         final boolean consumingOpaqueCaptionBar =
-                Flags.enableCaptionCompatInsetForceConsumptionAlways()
+                ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isEnabled()
                         && mLastForceConsumingOpaqueCaptionBar
                         && isOpaqueCaptionBar;
 
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 90cb10a..9a4ff8f 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -290,7 +290,6 @@
                 "libasync_safe",
                 "libbinderthreadstateutils",
                 "libdmabufinfo",
-                "libgif",
                 "libgui_window_info_static",
                 "libkernelconfigs",
                 "libnativehelper_lazy",
diff --git a/core/tests/coretests/src/android/window/flags/DesktopModeFlagsTest.java b/core/tests/coretests/src/android/window/flags/DesktopModeFlagsTest.java
index 32345e6..dd40695 100644
--- a/core/tests/coretests/src/android/window/flags/DesktopModeFlagsTest.java
+++ b/core/tests/coretests/src/android/window/flags/DesktopModeFlagsTest.java
@@ -16,7 +16,7 @@
 
 package android.window.flags;
 
-import static android.window.flags.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE;
 
 import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
 import static com.android.window.flags.Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS;
@@ -75,7 +75,7 @@
     public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() {
         setOverride(OVERRIDE_OFF_SETTING);
         // In absence of dev options, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
 
@@ -84,7 +84,7 @@
     public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() {
         setOverride(OVERRIDE_ON_SETTING);
 
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -93,7 +93,7 @@
         setOverride(OVERRIDE_UNSET_SETTING);
 
         // For overridableFlag, for unset overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
     @Test
@@ -103,7 +103,7 @@
         setOverride(OVERRIDE_UNSET_SETTING);
 
         // For overridableFlag, for unset overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -112,7 +112,7 @@
         setOverride(null);
 
         // For overridableFlag, in absence of overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
     @Test
@@ -122,7 +122,7 @@
         setOverride(null);
 
         // For overridableFlag, in absence of overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -131,7 +131,7 @@
         setOverride(-2);
 
         // For overridableFlag, for unrecognized overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
     @Test
@@ -141,7 +141,7 @@
         setOverride(-2);
 
         // For overridableFlag, for unrecognizable overrides, follow flag
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -150,7 +150,7 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // For overridableFlag, follow override if they exist
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -160,7 +160,7 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // For overridableFlag, follow override if they exist
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
     @Test
@@ -169,12 +169,12 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // For overridableFlag, follow override if they exist
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
 
         setOverride(OVERRIDE_ON_SETTING);
 
         // Keep overrides constant through the process
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isFalse();
     }
 
     @Test
@@ -184,12 +184,12 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // For overridableFlag, follow override if they exist
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
 
         setOverride(OVERRIDE_OFF_SETTING);
 
         // Keep overrides constant through the process
-        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isEnabled()).isTrue();
     }
 
     @Test
@@ -199,7 +199,7 @@
         setOverride(OVERRIDE_UNSET_SETTING);
 
         // For unset overrides, follow flag
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -208,7 +208,7 @@
     public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() {
         setOverride(OVERRIDE_UNSET_SETTING);
         // For unset overrides, follow flag
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     @Test
@@ -221,7 +221,7 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // When toggle override matches its default state (dw flag), don't override flags
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -231,7 +231,7 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // When toggle override matches its default state (dw flag), don't override flags
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     @Test
@@ -244,7 +244,7 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // Follow override if they exist, and is not equal to default toggle state (dw flag)
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -254,7 +254,7 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // Follow override if they exist, and is not equal to default toggle state (dw flag)
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     @Test
@@ -267,7 +267,7 @@
         setOverride(OVERRIDE_UNSET_SETTING);
 
         // For unset overrides, follow flag
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -280,7 +280,7 @@
         setOverride(OVERRIDE_UNSET_SETTING);
 
         // For unset overrides, follow flag
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     @Test
@@ -293,7 +293,7 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // Follow override if they exist, and is not equal to default toggle state (dw flag)
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -306,7 +306,7 @@
         setOverride(OVERRIDE_ON_SETTING);
 
         // Follow override if they exist, and is not equal to default toggle state (dw flag)
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     @Test
@@ -319,7 +319,7 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // When toggle override matches its default state (dw flag), don't override flags
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isTrue();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isTrue();
     }
 
     @Test
@@ -332,7 +332,7 @@
         setOverride(OVERRIDE_OFF_SETTING);
 
         // When toggle override matches its default state (dw flag), don't override flags
-        assertThat(DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)).isFalse();
+        assertThat(DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()).isFalse();
     }
 
     private void setOverride(Integer setting) {
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index e0823b8..d6b2a78 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -24,7 +24,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -190,25 +189,6 @@
         assertThat(generateSessionName(cujName, tooLongTag)).isEqualTo(expectedTrimmedName);
     }
 
-    @Test
-    public void validateConfiguration_surfaceOnlyAndNotDeferMonitor_throwsError() {
-        Configuration.Builder builder = Configuration.Builder.withSurface(1,
-                mActivity.getApplicationContext(), mSurfaceControl,
-                mActivity.getMainThreadHandler()).setDeferMonitorForAnimationStart(false);
-
-        assertThrows(IllegalStateException.class, builder::build);
-    }
-
-    @Test
-    public void validateConfiguration_surfaceOnlyAndDeferMonitor_doesNotThrowError() {
-        Configuration.Builder builder = Configuration.Builder.withSurface(1,
-                mActivity.getApplicationContext(),
-                mSurfaceControl, mActivity.getMainThreadHandler()).setDeferMonitorForAnimationStart(
-                true);
-
-        builder.build(); // no exception.
-    }
-
     private InteractionJankMonitor createMockedInteractionJankMonitor() {
         InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
         doReturn(true).when(monitor).shouldMonitor();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index b1fc55f..16036be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -24,6 +24,8 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
 
 import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
 import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
@@ -604,13 +606,13 @@
                 // their custom content.
                 relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
             } else {
-                if (Flags.enableCaptionCompatInsetForceConsumption()) {
+                if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isEnabled()) {
                     // Force-consume the caption bar insets when the app tries to hide the caption.
                     // This improves app compatibility of immersive apps.
                     relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING;
                 }
             }
-            if (Flags.enableCaptionCompatInsetForceConsumptionAlways()) {
+            if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isEnabled()) {
                 // Always force-consume the caption bar insets for maximum app compatibility,
                 // including non-immersive apps that just don't handle caption insets properly.
                 relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 1afef75..d993b87 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -64,25 +64,6 @@
     mLocked.pointerSprite.clear();
 }
 
-std::optional<FloatRect> MouseCursorController::getBounds() const {
-    std::scoped_lock lock(mLock);
-
-    return getBoundsLocked();
-}
-
-std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) {
-    if (!mLocked.viewport.isValid()) {
-        return {};
-    }
-
-    return FloatRect{
-            static_cast<float>(mLocked.viewport.logicalLeft),
-            static_cast<float>(mLocked.viewport.logicalTop),
-            static_cast<float>(mLocked.viewport.logicalRight - 1),
-            static_cast<float>(mLocked.viewport.logicalBottom - 1),
-    };
-}
-
 void MouseCursorController::move(float deltaX, float deltaY) {
 #if DEBUG_MOUSE_CURSOR_UPDATES
     ALOGD("Move pointer by deltaX=%0.3f, deltaY=%0.3f", deltaX, deltaY);
@@ -105,11 +86,20 @@
 }
 
 void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
-    const auto bounds = getBoundsLocked();
-    if (!bounds) return;
+    const auto& v = mLocked.viewport;
+    if (!v.isValid()) return;
 
-    mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x));
-    mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y));
+    // The valid bounds for a mouse cursor. Since the right and bottom edges are considered outside
+    // the display, clip the bounds by one pixel instead of letting the cursor get arbitrarily
+    // close to the outside edge.
+    const FloatRect bounds{
+            static_cast<float>(mLocked.viewport.logicalLeft),
+            static_cast<float>(mLocked.viewport.logicalTop),
+            static_cast<float>(mLocked.viewport.logicalRight - 1),
+            static_cast<float>(mLocked.viewport.logicalBottom - 1),
+    };
+    mLocked.pointerX = std::max(bounds.left, std::min(bounds.right, x));
+    mLocked.pointerY = std::max(bounds.top, std::min(bounds.bottom, y));
 
     updatePointerLocked();
 }
@@ -216,9 +206,11 @@
     // Reset cursor position to center if size or display changed.
     if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth ||
         oldDisplayHeight != newDisplayHeight) {
-        if (const auto bounds = getBoundsLocked(); bounds) {
-            mLocked.pointerX = (bounds->left + bounds->right) * 0.5f;
-            mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f;
+        if (viewport.isValid()) {
+            // Use integer coordinates as the starting point for the cursor location.
+            // We usually expect display sizes to be even numbers, so the flooring is precautionary.
+            mLocked.pointerX = std::floor((viewport.logicalLeft + viewport.logicalRight) / 2);
+            mLocked.pointerY = std::floor((viewport.logicalTop + viewport.logicalBottom) / 2);
             // Reload icon resources for density may be changed.
             loadResourcesLocked(getAdditionalMouseResources);
         } else {
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 8600341..12b31a8 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -43,7 +43,6 @@
     MouseCursorController(PointerControllerContext& context);
     ~MouseCursorController();
 
-    std::optional<FloatRect> getBounds() const;
     void move(float deltaX, float deltaY);
     void setPosition(float x, float y);
     FloatPoint getPosition() const;
@@ -104,7 +103,6 @@
 
     } mLocked GUARDED_BY(mLock);
 
-    std::optional<FloatRect> getBoundsLocked() const;
     void setPositionLocked(float x, float y);
 
     void updatePointerLocked();
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 5ae967b..78d7d3a 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -138,10 +138,6 @@
     return mDisplayInfoListener->mLock;
 }
 
-std::optional<FloatRect> PointerController::getBounds() const {
-    return mCursorController.getBounds();
-}
-
 void PointerController::move(float deltaX, float deltaY) {
     const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
     vec2 transformed;
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 4d1e1d7..ee8d121 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -51,7 +51,6 @@
 
     ~PointerController() override;
 
-    std::optional<FloatRect> getBounds() const override;
     void move(float deltaX, float deltaY) override;
     void setPosition(float x, float y) override;
     FloatPoint getPosition() const override;
@@ -166,9 +165,6 @@
 
     ~TouchPointerController() override;
 
-    std::optional<FloatRect> getBounds() const override {
-        LOG_ALWAYS_FATAL("Should not be called");
-    }
     void move(float, float) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 682a68f..a26cf12 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -23,9 +23,7 @@
 }
 
 java_library {
-
     name: "SystemUIPluginLib",
-
     srcs: [
         "bcsmartspace/src/**/*.java",
         "bcsmartspace/src/**/*.kt",
@@ -40,6 +38,8 @@
         export_proguard_flags_files: true,
     },
 
+    plugins: ["PluginAnnotationProcessor"],
+
     // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter
     // in PluginInstance. That will ensure that loaded plugins have access to the related classes.
     // You should also add it to proguard_common.flags so that proguard does not remove the portions
@@ -53,7 +53,6 @@
         "SystemUILogLib",
         "androidx.annotation_annotation",
     ],
-
 }
 
 android_app {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8dc4815..6d27b6f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -21,7 +21,11 @@
 import com.android.internal.annotations.Keep
 import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.Plugin
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -31,6 +35,7 @@
 typealias ClockId = String
 
 /** A Plugin which exposes the ClockProvider interface */
+@ProtectedInterface
 @ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
 interface ClockProviderPlugin : Plugin, ClockProvider {
     companion object {
@@ -40,31 +45,42 @@
 }
 
 /** Interface for building clocks and providing information about those clocks */
+@ProtectedInterface
+@GeneratedImport("java.util.List")
+@GeneratedImport("java.util.ArrayList")
 interface ClockProvider {
     /** Initializes the clock provider with debug log buffers */
     fun initialize(buffers: ClockMessageBuffers?)
 
+    @ProtectedReturn("return new ArrayList<ClockMetadata>();")
     /** Returns metadata for all clocks this provider knows about */
     fun getClocks(): List<ClockMetadata>
 
+    @ProtectedReturn("return null;")
     /** Initializes and returns the target clock design */
-    fun createClock(settings: ClockSettings): ClockController
+    fun createClock(settings: ClockSettings): ClockController?
 
+    @ProtectedReturn("return new ClockPickerConfig(\"\", \"\", \"\", null);")
     /** Settings configuration parameters for the clock */
     fun getClockPickerConfig(id: ClockId): ClockPickerConfig
 }
 
 /** Interface for controlling an active clock */
+@ProtectedInterface
 interface ClockController {
+    @get:SimpleProperty
     /** A small version of the clock, appropriate for smaller viewports */
     val smallClock: ClockFaceController
 
+    @get:SimpleProperty
     /** A large version of the clock, appropriate when a bigger viewport is available */
     val largeClock: ClockFaceController
 
+    @get:SimpleProperty
     /** Determines the way the hosting app should behave when rendering either clock face */
     val config: ClockConfig
 
+    @get:SimpleProperty
     /** Events that clocks may need to respond to */
     val events: ClockEvents
 
@@ -76,19 +92,26 @@
 }
 
 /** Interface for a specific clock face version rendered by the clock */
+@ProtectedInterface
 interface ClockFaceController {
+    @get:SimpleProperty
+    @Deprecated("Prefer use of layout")
     /** View that renders the clock face */
     val view: View
 
+    @get:SimpleProperty
     /** Layout specification for this clock */
     val layout: ClockFaceLayout
 
+    @get:SimpleProperty
     /** Determines the way the hosting app should behave when rendering this clock face */
     val config: ClockFaceConfig
 
+    @get:SimpleProperty
     /** Events specific to this clock face */
     val events: ClockFaceEvents
 
+    @get:SimpleProperty
     /** Triggers for various animations */
     val animations: ClockAnimations
 }
@@ -107,14 +130,21 @@
 
 data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
 
-/** Specifies layout information for the */
+/** Specifies layout information for the clock face */
+@ProtectedInterface
+@GeneratedImport("java.util.ArrayList")
+@GeneratedImport("android.view.View")
 interface ClockFaceLayout {
+    @get:ProtectedReturn("return new ArrayList<View>();")
     /** All clock views to add to the root constraint layout before applying constraints. */
     val views: List<View>
 
+    @ProtectedReturn("return constraints;")
     /** Custom constraints to apply to Lockscreen ConstraintLayout. */
     fun applyConstraints(constraints: ConstraintSet): ConstraintSet
 
+    @ProtectedReturn("return constraints;")
+    /** Custom constraints to apply to preview ConstraintLayout. */
     fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
 
     fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
@@ -145,7 +175,9 @@
 }
 
 /** Events that should call when various rendering parameters change */
+@ProtectedInterface
 interface ClockEvents {
+    @get:ProtectedReturn("return false;")
     /** Set to enable or disable swipe interaction */
     var isReactiveTouchInteractionEnabled: Boolean
 
@@ -187,6 +219,7 @@
 )
 
 /** Methods which trigger various clock animations */
+@ProtectedInterface
 interface ClockAnimations {
     /** Runs an enter animation (if any) */
     fun enter()
@@ -230,6 +263,7 @@
 }
 
 /** Events that have specific data about the related face */
+@ProtectedInterface
 interface ClockFaceEvents {
     /** Call every time tick */
     fun onTimeTick()
@@ -270,7 +304,9 @@
 /** Some data about a clock design */
 data class ClockMetadata(val clockId: ClockId)
 
-data class ClockPickerConfig(
+data class ClockPickerConfig
+@JvmOverloads
+constructor(
     val id: String,
 
     /** Localized name of the clock */
@@ -338,7 +374,7 @@
     /** Transition to AOD should move smartspace like large clock instead of small clock */
     val useAlternateSmartspaceAODTransition: Boolean = false,
 
-    /** Use ClockPickerConfig.isReactiveToTone instead */
+    /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
     @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
     val isReactiveToTone: Boolean = true,
 
diff --git a/packages/SystemUI/plugin_core/Android.bp b/packages/SystemUI/plugin_core/Android.bp
index 521c019..31fbda5 100644
--- a/packages/SystemUI/plugin_core/Android.bp
+++ b/packages/SystemUI/plugin_core/Android.bp
@@ -24,8 +24,13 @@
 
 java_library {
     sdk_version: "current",
-    name: "PluginCoreLib",
-    srcs: ["src/**/*.java"],
+    name: "PluginAnnotationLib",
+    host_supported: true,
+    device_supported: true,
+    srcs: [
+        "src/**/annotations/*.java",
+        "src/**/annotations/*.kt",
+    ],
     optimize: {
         proguard_flags_files: ["proguard.flags"],
         // Ensure downstream clients that reference this as a shared lib
@@ -37,3 +42,59 @@
     // no compatibility issues with launcher
     java_version: "1.8",
 }
+
+java_library {
+    sdk_version: "current",
+    name: "PluginCoreLib",
+    device_supported: true,
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    exclude_srcs: [
+        "src/**/annotations/*.java",
+        "src/**/annotations/*.kt",
+        "src/**/processor/*.java",
+        "src/**/processor/*.kt",
+    ],
+    static_libs: [
+        "PluginAnnotationLib",
+    ],
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+        // Ensure downstream clients that reference this as a shared lib
+        // inherit the appropriate flags to preserve annotations.
+        export_proguard_flags_files: true,
+    },
+
+    // Enforce that the library is built against java 8 so that there are
+    // no compatibility issues with launcher
+    java_version: "1.8",
+}
+
+java_library {
+    java_version: "1.8",
+    name: "PluginAnnotationProcessorLib",
+    host_supported: true,
+    device_supported: false,
+    srcs: [
+        "src/**/processor/*.java",
+        "src/**/processor/*.kt",
+    ],
+    plugins: ["auto_service_plugin"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "auto_service_annotations",
+        "auto_common",
+        "PluginAnnotationLib",
+        "guava",
+        "jsr330",
+    ],
+}
+
+java_plugin {
+    name: "PluginAnnotationProcessor",
+    processor_class: "com.android.systemui.plugins.processor.ProtectedPluginProcessor",
+    static_libs: ["PluginAnnotationProcessorLib"],
+    java_version: "1.8",
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
index 8ff6c11..84040f9 100644
--- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
@@ -15,6 +15,7 @@
 
 import android.content.Context;
 
+import com.android.systemui.plugins.annotations.ProtectedReturn;
 import com.android.systemui.plugins.annotations.Requires;
 
 /**
@@ -116,6 +117,8 @@
      * @deprecated
      * @see Requires
      */
+    @Deprecated
+    @ProtectedReturn(statement = "return -1;")
     default int getVersion() {
         // Default of -1 indicates the plugin supports the new Requires model.
         return -1;
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
new file mode 100644
index 0000000..425d00a
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins
+
+/** Listener for events from proxy types generated by [ProtectedPluginProcessor]. */
+interface ProtectedPluginListener {
+    /**
+     * Called when a method call produces a [LinkageError] before returning. This callback is
+     * provided so that the host application can terminate the plugin or log the error as
+     * appropraite.
+     *
+     * @return true to terminate all methods within this object; false if the error is recoverable
+     *   and the proxied plugin should continue to operate as normal.
+     */
+    fun onFail(className: String, methodName: String, failure: LinkageError): Boolean
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
new file mode 100644
index 0000000..12a977d
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins.annotations
+
+/**
+ * This annotation marks denotes that an interface should use a proxy layer to protect the plugin
+ * host from crashing due to [LinkageError]s originating within the plugin's implementation.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedInterface
+
+/**
+ * This annotation specifies any additional imports that the processor will require when generating
+ * the proxy implementation for the target interface. The interface in question must still be
+ * annotated with [ProtectedInterface].
+ */
+@Repeatable
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class GeneratedImport(val extraImport: String)
+
+/**
+ * This annotation provides default values to return when the proxy implementation catches a
+ * [LinkageError]. The string specified should be a simple but valid java statement. In most cases
+ * it should be a return statement of the appropriate type, but in some cases throwing a known
+ * exception type may be preferred.
+ *
+ * This annotation is not required for methods that return void, but will behave the same way.
+ */
+@Target(
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY,
+    AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedReturn(val statement: String)
+
+/**
+ * Some very simple properties and methods need not be protected by the proxy implementation. This
+ * annotation can be used to omit the normal try-catch wrapper the proxy is using. These members
+ * will instead be a direct passthrough.
+ *
+ * It should only be used for members where the plugin implementation is expected to be exceedingly
+ * simple. Any member marked with this annotation should be no more complex than kotlin's automatic
+ * properties, and make no other method calls whatsoever.
+ */
+@Target(
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY,
+    AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class SimpleProperty
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
new file mode 100644
index 0000000..8266de5
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins.processor
+
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import com.android.systemui.plugins.annotations.SimpleProperty
+import com.google.auto.service.AutoService
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.PackageElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic.Kind
+import kotlin.collections.ArrayDeque
+
+/**
+ * [ProtectedPluginProcessor] generates a proxy implementation for interfaces annotated with
+ * [ProtectedInterface] which catches [LinkageError]s generated by the proxied target. This protects
+ * the plugin host from crashing due to out-of-date plugin code, where some call has changed so that
+ * the [ClassLoader] can no longer resolve it correctly.
+ *
+ * [PluginInstance] observes these failures via [ProtectedMethodListener] and unloads the plugin in
+ * question to prevent further issues. This persists through further load/unload requests.
+ *
+ * To centralize access to the proxy types, an additional type [PluginProtector] is also generated.
+ * This class provides static methods which wrap an instance of the target interface in the proxy
+ * type if it is not already an instance of the proxy.
+ */
+@AutoService(ProtectedPluginProcessor::class)
+class ProtectedPluginProcessor : AbstractProcessor() {
+    private lateinit var procEnv: ProcessingEnvironment
+
+    override fun init(procEnv: ProcessingEnvironment) {
+        this.procEnv = procEnv
+    }
+
+    override fun getSupportedAnnotationTypes(): Set<String> =
+        setOf("com.android.systemui.plugins.annotations.ProtectedInterface")
+
+    private data class TargetData(
+        val attribute: TypeElement,
+        val sourceType: Element,
+        val sourcePkg: String,
+        val sourceName: String,
+        val outputName: String,
+    )
+
+    override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+        val targets = mutableMapOf<String, TargetData>() // keyed by fully-qualified source name
+        val additionalImports = mutableSetOf<String>()
+        for (attr in annotations) {
+            for (target in roundEnv.getElementsAnnotatedWith(attr)) {
+                val sourceName = "${target.simpleName}"
+                val outputName = "${sourceName}Protector"
+                val pkg = (target.getEnclosingElement() as PackageElement).qualifiedName.toString()
+                targets.put("$target", TargetData(attr, target, pkg, sourceName, outputName))
+
+                // This creates excessive imports, but it should be fine
+                additionalImports.add("$pkg.$sourceName")
+                additionalImports.add("$pkg.$outputName")
+            }
+        }
+
+        if (targets.size <= 0) return false
+        for ((_, sourceType, sourcePkg, sourceName, outputName) in targets.values) {
+            // Find all methods in this type and all super types to that need to be implemented
+            val types = ArrayDeque<TypeMirror>().apply { addLast(sourceType.asType()) }
+            val impAttrs = mutableListOf<GeneratedImport>()
+            val methods = mutableListOf<ExecutableElement>()
+            while (types.size > 0) {
+                val typeMirror = types.removeLast()
+                if (typeMirror.toString() == "java.lang.Object") continue
+                val type = procEnv.typeUtils.asElement(typeMirror)
+                for (member in type.enclosedElements) {
+                    if (member.kind != ElementKind.METHOD) continue
+                    methods.add(member as ExecutableElement)
+                }
+
+                impAttrs.addAll(type.getAnnotationsByType(GeneratedImport::class.java))
+                types.addAll(procEnv.typeUtils.directSupertypes(typeMirror))
+            }
+
+            val file = procEnv.filer.createSourceFile("$outputName")
+            TabbedWriter.writeTo(file.openWriter()) {
+                line("package $sourcePkg;")
+                line()
+
+                // Imports used by the proxy implementation
+                line("import android.util.Log;")
+                line("import java.lang.LinkageError;")
+                line("import com.android.systemui.plugins.ProtectedPluginListener;")
+                line()
+
+                // Imports of other generated types
+                if (additionalImports.size > 0) {
+                    for (impTarget in additionalImports) {
+                        line("import $impTarget;")
+                    }
+                    line()
+                }
+
+                // Imports declared via @GeneratedImport
+                if (impAttrs.size > 0) {
+                    for (impAttr in impAttrs) {
+                        line("import ${impAttr.extraImport};")
+                    }
+                    line()
+                }
+
+                braceBlock("public class $outputName implements $sourceName") {
+                    line("private static final String CLASS = \"$sourceName\";")
+
+                    // Static factory method to prevent wrapping the same object twice
+                    parenBlock("public static $outputName protect") {
+                        line("$sourceName instance,")
+                        line("ProtectedPluginListener listener")
+                    }
+                    braceBlock {
+                        line("if (instance instanceof $outputName)")
+                        line("    return ($outputName)instance;")
+                        line("return new $outputName(instance, listener);")
+                    }
+                    line()
+
+                    // Member Fields
+                    line("private $sourceName mInstance;")
+                    line("private ProtectedPluginListener mListener;")
+                    line("private boolean mHasError = false;")
+                    line()
+
+                    // Constructor
+                    parenBlock("private $outputName") {
+                        line("$sourceName instance,")
+                        line("ProtectedPluginListener listener")
+                    }
+                    braceBlock {
+                        line("mInstance = instance;")
+                        line("mListener = listener;")
+                    }
+                    line()
+
+                    // Method implementations
+                    for (method in methods) {
+                        val methodName = method.simpleName
+                        val returnTypeName = method.returnType.toString()
+                        val callArgs = StringBuilder()
+                        var isFirst = true
+
+                        line("@Override")
+                        parenBlock("public $returnTypeName $methodName") {
+                            // While copying the method signature for the proxy type, we also
+                            // accumulate arguments for the nested callsite.
+                            for (param in method.parameters) {
+                                if (!isFirst) completeLine(",")
+                                startLine("${param.asType()} ${param.simpleName}")
+                                isFirst = false
+
+                                if (callArgs.length > 0) callArgs.append(", ")
+                                callArgs.append(param.simpleName)
+                            }
+                        }
+
+                        val isVoid = method.returnType.kind == TypeKind.VOID
+                        val nestedCall = "mInstance.$methodName($callArgs)"
+                        val callStatement =
+                            when {
+                                isVoid -> "$nestedCall;"
+                                targets.containsKey(returnTypeName) -> {
+                                    val targetType = targets.get(returnTypeName)!!.outputName
+                                    "return $targetType.protect($nestedCall, mListener);"
+                                }
+                                else -> "return $nestedCall;"
+                            }
+
+                        // Simple property methods forgo protection
+                        val simpleAttr = method.getAnnotation(SimpleProperty::class.java)
+                        if (simpleAttr != null) {
+                            braceBlock {
+                                line("final String METHOD = \"$methodName\";")
+                                line(callStatement)
+                            }
+                            line()
+                            continue
+                        }
+
+                        // Standard implementation wraps nested call in try-catch
+                        braceBlock {
+                            val retAttr = method.getAnnotation(ProtectedReturn::class.java)
+                            val errorStatement =
+                                when {
+                                    retAttr != null -> retAttr.statement
+                                    isVoid -> "return;"
+                                    else -> {
+                                        // Non-void methods must be annotated.
+                                        procEnv.messager.printMessage(
+                                            Kind.ERROR,
+                                            "$outputName.$methodName must be annotated with " +
+                                                "@ProtectedReturn or @SimpleProperty",
+                                        )
+                                        "throw ex;"
+                                    }
+                                }
+
+                            line("final String METHOD = \"$methodName\";")
+
+                            // Return immediately if any previous call has failed.
+                            braceBlock("if (mHasError)") { line(errorStatement) }
+
+                            // Protect callsite in try/catch block
+                            braceBlock("try") { line(callStatement) }
+
+                            // Notify listener when a LinkageError is caught
+                            braceBlock("catch (LinkageError ex)") {
+                                line("Log.wtf(CLASS, \"Failed to execute: \" + METHOD, ex);")
+                                line("mHasError = mListener.onFail(CLASS, METHOD, ex);")
+                                line(errorStatement)
+                            }
+                        }
+                        line()
+                    }
+                }
+            }
+        }
+
+        // Write a centralized static factory type to its own file. This is for convience so that
+        // PluginInstance need not resolve each generated type at runtime as plugins are loaded.
+        val factoryFile = procEnv.filer.createSourceFile("PluginProtector")
+        TabbedWriter.writeTo(factoryFile.openWriter()) {
+            line("package com.android.systemui.plugins;")
+            line()
+
+            line("import java.util.Map;")
+            line("import java.util.ArrayList;")
+            line("import java.util.HashSet;")
+            line("import static java.util.Map.entry;")
+            line()
+
+            for (impTarget in additionalImports) {
+                line("import $impTarget;")
+            }
+            line()
+
+            braceBlock("public final class PluginProtector") {
+                line("private PluginProtector() { }")
+                line()
+
+                // Untyped factory SAM, private to this type.
+                braceBlock("private interface Factory") {
+                    line("Object create(Object plugin, ProtectedPluginListener listener);")
+                }
+                line()
+
+                // Store a reference to each `protect` method in a map by interface type.
+                parenBlock("private static final Map<Class, Factory> sFactories = Map.ofEntries") {
+                    var isFirst = true
+                    for (target in targets.values) {
+                        if (!isFirst) completeLine(",")
+                        target.apply {
+                            startLine("entry($sourceName.class, ")
+                            appendLine("(p, h) -> $outputName.protect(($sourceName)p, h))")
+                        }
+                        isFirst = false
+                    }
+                }
+                completeLine(";")
+                line()
+
+                // Lookup the relevant factory based on the instance type, if not found return null.
+                parenBlock("public static <T> T tryProtect") {
+                    line("T target,")
+                    line("ProtectedPluginListener listener")
+                }
+                braceBlock {
+                    // Accumulate interfaces from type and all base types
+                    line("HashSet<Class> interfaces = new HashSet<Class>();")
+                    line("Class current = target.getClass();")
+                    braceBlock("while (current != null)") {
+                        braceBlock("for (Class cls : current.getInterfaces())") {
+                            line("interfaces.add(cls);")
+                        }
+                        line("current = current.getSuperclass();")
+                    }
+                    line()
+
+                    // Check if any of the interfaces are marked protectable
+                    line("int candidateCount = 0;")
+                    line("Factory candidateFactory = null;")
+                    braceBlock("for (Class cls : interfaces)") {
+                        line("Factory factory = sFactories.get(cls);")
+                        braceBlock("if (factory != null)") {
+                            line("candidateFactory = factory;")
+                            line("candidateCount++;")
+                        }
+                    }
+                    line()
+
+                    // No match, return null
+                    braceBlock("if (candidateFactory == null)") { line("return null;") }
+
+                    // Multiple matches, not supported
+                    braceBlock("if (candidateCount >= 2)") {
+                        var error = "Plugin implements more than one protected interface"
+                        line("throw new UnsupportedOperationException(\"$error\");")
+                    }
+
+                    // Call the factory and wrap the target object
+                    line("return (T)candidateFactory.create(target, listener);")
+                }
+                line()
+
+                // Wraps the target with the appropriate generated proxy if it exists.
+                parenBlock("public static <T> T protectIfAble") {
+                    line("T target,")
+                    line("ProtectedPluginListener listener")
+                }
+                braceBlock {
+                    line("T result = tryProtect(target, listener);")
+                    line("return result != null ? result : target;")
+                }
+                line()
+            }
+        }
+
+        return true
+    }
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
new file mode 100644
index 0000000..941b2c2
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins.processor
+
+import java.io.BufferedWriter
+import java.io.Writer
+
+/**
+ * [TabbedWriter] is a convience class which tracks and writes correctly tabbed lines for generating
+ * source files. These files don't need to be correctly tabbed as they're ephemeral and not part of
+ * the source tree, but correct tabbing makes debugging much easier when the build fails.
+ */
+class TabbedWriter(writer: Writer) : AutoCloseable {
+    private val target = BufferedWriter(writer)
+    private var isInProgress = false
+    var tabCount: Int = 0
+        private set
+
+    override fun close() = target.close()
+
+    fun line() {
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun line(str: String) {
+        if (isInProgress) {
+            target.newLine()
+        }
+
+        target.append("    ".repeat(tabCount))
+        target.append(str)
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun completeLine(str: String) {
+        if (!isInProgress) {
+            target.newLine()
+            target.append("    ".repeat(tabCount))
+        }
+
+        target.append(str)
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun startLine(str: String) {
+        if (isInProgress) {
+            target.newLine()
+        }
+
+        target.append("    ".repeat(tabCount))
+        target.append(str)
+        isInProgress = true
+    }
+
+    fun appendLine(str: String) {
+        if (!isInProgress) {
+            target.append("    ".repeat(tabCount))
+        }
+
+        target.append(str)
+        isInProgress = true
+    }
+
+    fun braceBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+        block(str, " {", "}", true, write)
+    }
+
+    fun parenBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+        block(str, "(", ")", false, write)
+    }
+
+    private fun block(
+        str: String,
+        start: String,
+        end: String,
+        newLineForEnd: Boolean,
+        write: TabbedWriter.() -> Unit,
+    ) {
+        appendLine(str)
+        completeLine(start)
+
+        tabCount++
+        this.write()
+        tabCount--
+
+        if (newLineForEnd) {
+            line(end)
+        } else {
+            startLine(end)
+        }
+    }
+
+    companion object {
+        fun writeTo(writer: Writer, write: TabbedWriter.() -> Unit) {
+            TabbedWriter(writer).use { it.write() }
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 87cc86f..5a9e021 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -32,6 +32,8 @@
 import com.android.systemui.plugins.PluginFragment;
 import com.android.systemui.plugins.PluginLifecycleManager;
 import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginProtector;
+import com.android.systemui.plugins.ProtectedPluginListener;
 
 import dalvik.system.PathClassLoader;
 
@@ -49,7 +51,8 @@
  *
  * @param <T> The type of plugin that this contains.
  */
-public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
+public class PluginInstance<T extends Plugin>
+        implements PluginLifecycleManager, ProtectedPluginListener {
     private static final String TAG = "PluginInstance";
 
     private final Context mAppContext;
@@ -58,6 +61,7 @@
     private final PluginFactory<T> mPluginFactory;
     private final String mTag;
 
+    private boolean mHasError = false;
     private BiConsumer<String, String> mLogConsumer = null;
     private Context mPluginContext;
     private T mPlugin;
@@ -87,6 +91,11 @@
         return mTag;
     }
 
+    /** */
+    public boolean hasError() {
+        return mHasError;
+    }
+
     public void setLogFunc(BiConsumer logConsumer) {
         mLogConsumer = logConsumer;
     }
@@ -97,8 +106,21 @@
         }
     }
 
+    @Override
+    public synchronized boolean onFail(String className, String methodName, LinkageError failure) {
+        mHasError = true;
+        unloadPlugin();
+        mListener.onPluginDetached(this);
+        return true;
+    }
+
     /** Alerts listener and plugin that the plugin has been created. */
     public synchronized void onCreate() {
+        if (mHasError) {
+            log("Previous LinkageError detected for plugin class");
+            return;
+        }
+
         boolean loadPlugin = mListener.onPluginAttached(this);
         if (!loadPlugin) {
             if (mPlugin != null) {
@@ -126,6 +148,12 @@
 
     /** Alerts listener and plugin that the plugin is being shutdown. */
     public synchronized void onDestroy() {
+        if (mHasError) {
+            // Detached in error handler
+            log("onDestroy - no-op");
+            return;
+        }
+
         log("onDestroy");
         unloadPlugin();
         mListener.onPluginDetached(this);
@@ -134,20 +162,25 @@
     /** Returns the current plugin instance (if it is loaded). */
     @Nullable
     public T getPlugin() {
-        return mPlugin;
+        return mHasError ? null : mPlugin;
     }
 
     /**
      * Loads and creates the plugin if it does not exist.
      */
     public synchronized void loadPlugin() {
+        if (mHasError) {
+            log("Previous LinkageError detected for plugin class");
+            return;
+        }
+
         if (mPlugin != null) {
             log("Load request when already loaded");
             return;
         }
 
         // Both of these calls take about 1 - 1.5 seconds in test runs
-        mPlugin = mPluginFactory.createPlugin();
+        mPlugin = mPluginFactory.createPlugin(this);
         mPluginContext = mPluginFactory.createPluginContext();
         if (mPlugin == null || mPluginContext == null) {
             Log.e(mTag, "Requested load, but failed");
@@ -364,20 +397,16 @@
         }
 
         /** Creates the related plugin object from the factory */
-        public T createPlugin() {
+        public T createPlugin(ProtectedPluginListener listener) {
             try {
                 ClassLoader loader = mClassLoaderFactory.get();
                 Class<T> instanceClass = (Class<T>) Class.forName(
                         mComponentName.getClassName(), true, loader);
                 T result = (T) mInstanceFactory.create(instanceClass);
                 Log.v(TAG, "Created plugin: " + result);
-                return result;
-            } catch (ClassNotFoundException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
-            } catch (IllegalAccessException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
-            } catch (InstantiationException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
+                return PluginProtector.protectIfAble(result, listener);
+            } catch (ReflectiveOperationException ex) {
+                Log.wtf(TAG, "Failed to load plugin", ex);
             }
             return null;
         }
@@ -397,7 +426,7 @@
         /** Check Version and create VersionInfo for instance */
         public VersionInfo checkVersion(T instance) {
             if (instance == null) {
-                instance = createPlugin();
+                instance = createPlugin(null);
             }
             return mVersionChecker.checkVersion(
                     (Class<T>) instance.getClass(), mPluginClass, instance);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index aa6c08e..45aad82 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -140,6 +140,7 @@
                     }
                 },
                 onLongClick = { tile.onLongClick(expandable) },
+                accessibilityUiState = uiState.accessibilityUiState,
             )
         }
     }
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index cc6904f..156d8a0 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -103,7 +103,7 @@
         final TaskDisplayArea displayArea = task.getDisplayArea();
         final Rect screenBounds = displayArea.getBounds();
         final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        if (!DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(activity.mWmService.mContext)) {
+        if (!DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isEnabled()) {
             return centerInScreen(idealSize, screenBounds);
         }
         if (activity.mAppCompatController.getAppCompatAspectRatioOverrides()
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index 61fbb96..da76317 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -35,8 +35,8 @@
             "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
 
     /** Whether desktop mode is enabled. */
-    static boolean isDesktopModeEnabled(@NonNull Context context) {
-        return DesktopModeFlags.DESKTOP_WINDOWING_MODE.isEnabled(context);
+    static boolean isDesktopModeEnabled() {
+        return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isEnabled();
     }
 
     /**
@@ -60,7 +60,7 @@
      * Return {@code true} if desktop mode can be entered on the current device.
      */
     static boolean canEnterDesktopMode(@NonNull Context context) {
-        return isDesktopModeEnabled(context)
+        return isDesktopModeEnabled()
                 && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context));
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9d46529..7c3f0f2 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -35,6 +35,7 @@
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
+import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
@@ -115,7 +116,6 @@
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
 import com.android.server.wm.utils.AlwaysTruePredicate;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -457,7 +457,7 @@
         source.setFrame(provider.getArbitraryRectangle())
                 .updateSideHint(getBounds())
                 .setBoundingRects(provider.getBoundingRects());
-        if (Flags.enableCaptionCompatInsetForceConsumption()) {
+        if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isEnabled()) {
             source.setFlags(provider.getFlags());
         }
         mLocalInsetsSources.put(id, source);
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 8c4448e..155e73c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -1808,9 +1808,30 @@
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
 
-    for (int32_t iconId = static_cast<int32_t>(PointerIconStyle::TYPE_CONTEXT_MENU);
-         iconId <= static_cast<int32_t>(PointerIconStyle::TYPE_HANDWRITING); ++iconId) {
-        const PointerIconStyle pointerIconStyle = static_cast<PointerIconStyle>(iconId);
+    constexpr static std::array ADDITIONAL_STYLES{PointerIconStyle::TYPE_CONTEXT_MENU,
+                                                  PointerIconStyle::TYPE_HAND,
+                                                  PointerIconStyle::TYPE_HELP,
+                                                  PointerIconStyle::TYPE_WAIT,
+                                                  PointerIconStyle::TYPE_CELL,
+                                                  PointerIconStyle::TYPE_CROSSHAIR,
+                                                  PointerIconStyle::TYPE_TEXT,
+                                                  PointerIconStyle::TYPE_VERTICAL_TEXT,
+                                                  PointerIconStyle::TYPE_ALIAS,
+                                                  PointerIconStyle::TYPE_COPY,
+                                                  PointerIconStyle::TYPE_NO_DROP,
+                                                  PointerIconStyle::TYPE_ALL_SCROLL,
+                                                  PointerIconStyle::TYPE_HORIZONTAL_DOUBLE_ARROW,
+                                                  PointerIconStyle::TYPE_VERTICAL_DOUBLE_ARROW,
+                                                  PointerIconStyle::TYPE_TOP_RIGHT_DOUBLE_ARROW,
+                                                  PointerIconStyle::TYPE_TOP_LEFT_DOUBLE_ARROW,
+                                                  PointerIconStyle::TYPE_ZOOM_IN,
+                                                  PointerIconStyle::TYPE_ZOOM_OUT,
+                                                  PointerIconStyle::TYPE_GRAB,
+                                                  PointerIconStyle::TYPE_GRABBING,
+                                                  PointerIconStyle::TYPE_HANDWRITING,
+                                                  PointerIconStyle::TYPE_SPOT_HOVER};
+
+    for (const auto pointerIconStyle : ADDITIONAL_STYLES) {
         PointerIcon pointerIcon = loadPointerIcon(env, displayId, pointerIconStyle);
         (*outResources)[pointerIconStyle] = toSpriteIcon(pointerIcon);
         if (!pointerIcon.bitmapFrames.empty()) {
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
index 290e7be..9467434 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
@@ -22,6 +22,7 @@
 import com.google.android.lint.aidl.EnforcePermissionDetector
 import com.google.android.lint.aidl.PermissionAnnotationDetector
 import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
+import com.google.android.lint.aidl.SimpleRequiresNoPermissionDetector
 import com.google.auto.service.AutoService
 
 @AutoService(IssueRegistry::class)
@@ -34,6 +35,7 @@
             EnforcePermissionDetector.ISSUE_MISUSING_ENFORCE_PERMISSION,
             PermissionAnnotationDetector.ISSUE_MISSING_PERMISSION_ANNOTATION,
             SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
+            SimpleRequiresNoPermissionDetector.ISSUE_SIMPLE_REQUIRES_NO_PERMISSION,
     )
 
     override val api: Int
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetector.kt
new file mode 100644
index 0000000..1a13c02
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetector.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UastCallKind
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * Ensures all AIDL implementations hosted by system_server which don't call other methods are
+ * annotated with @RequiresNoPermission. AIDL Interfaces part of `exemptAidlInterfaces` are skipped
+ * during this search to ensure the detector targets only new AIDL Interfaces.
+ */
+class SimpleRequiresNoPermissionDetector : AidlImplementationDetector() {
+    override fun visitAidlMethod(
+        context: JavaContext,
+        node: UMethod,
+        interfaceName: String,
+        body: UBlockExpression
+    ) {
+        if (!isSystemServicePath(context)) return
+        if (context.evaluator.isAbstract(node)) return
+
+        val fullyQualifiedInterfaceName =
+            getContainingAidlInterfaceQualified(context, node) ?: return
+        if (exemptAidlInterfaces.contains(fullyQualifiedInterfaceName)) return
+
+        if (node.hasAnnotation(ANNOTATION_REQUIRES_NO_PERMISSION)) return
+
+        if (!isCallingMethod(node)) {
+            context.report(
+                ISSUE_SIMPLE_REQUIRES_NO_PERMISSION,
+                node,
+                context.getLocation(node),
+                """
+                    Method ${node.name} doesn't perform any permission checks, meaning it should \
+                    be annotated with @RequiresNoPermission.
+                """.trimMargin()
+            )
+        }
+    }
+
+    private fun isCallingMethod(node: UMethod): Boolean {
+        val uCallExpressionVisitor = UCallExpressionVisitor()
+        node.accept(uCallExpressionVisitor)
+
+        return uCallExpressionVisitor.isCallingMethod
+    }
+
+    /**
+     * Visits the body of a `UMethod` and determines if it encounters a `UCallExpression` which is
+     * a `UastCallKind.METHOD_CALL`. `isCallingMethod` will hold the result of the search procedure.
+     */
+    private class UCallExpressionVisitor : AbstractUastVisitor() {
+        var isCallingMethod = false
+
+        override fun visitElement(node: UElement): Boolean {
+            // Stop the search early when a method call has been found.
+            return isCallingMethod
+        }
+
+        override fun visitCallExpression(node: UCallExpression): Boolean {
+            if (node.kind != UastCallKind.METHOD_CALL) return false
+
+            isCallingMethod = true
+            return true
+        }
+    }
+
+    companion object {
+
+        private val EXPLANATION = """
+            Method implementations of AIDL Interfaces hosted by the `system_server` which do not
+            call any other methods should be annotated with @RequiresNoPermission. That is because
+            not calling any other methods implies that the method does not perform any permission
+            checking.
+
+            Please migrate to an @RequiresNoPermission annotation.
+        """.trimIndent()
+
+        @JvmField
+        val ISSUE_SIMPLE_REQUIRES_NO_PERMISSION = Issue.create(
+            id = "SimpleRequiresNoPermission",
+            briefDescription = "System Service APIs not calling other methods should use @RNP",
+            explanation = EXPLANATION,
+            category = Category.SECURITY,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                SimpleRequiresNoPermissionDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+        )
+    }
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
index 92d0829..824be93 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
@@ -17,7 +17,6 @@
 package com.google.android.lint.aidl
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -64,7 +63,7 @@
                     """
                         package com.android.server;
                         public class Bar extends IBar.Stub {
-                            public void testMethod() { }
+                            public void testMethod(int parameter1, int parameter2) { }
                         }
                     """
                 )
@@ -75,8 +74,8 @@
             .expect(
                 """
                 src/frameworks/base/services/java/com/android/server/Bar.java:3: Error: The method testMethod is not permission-annotated. [MissingPermissionAnnotation]
-                    public void testMethod() { }
-                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    public void testMethod(int parameter1, int parameter2) { }
+                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 1 errors, 0 warnings
                 """
             )
@@ -90,7 +89,7 @@
                     """
                         package com.android.server;
                         public class Bar extends IBar.Stub {
-                            public void testMethod() { }
+                            public void testMethod(int parameter1, int parameter2) { }
                         }
                     """
                 )
@@ -132,7 +131,7 @@
                     """
                         package com.android.server;
                         public abstract class Bar extends IBar.Stub {
-                            public abstract void testMethod();
+                            public abstract void testMethod(int parameter1, int parameter2);
                         }
                     """
                 )
@@ -177,50 +176,6 @@
             .expectClean()
     }
 
-    /* Stubs */
-
-    // A service with permission annotation on the method.
-    private val interfaceIFoo: TestFile = java(
-        """
-        public interface IFoo extends android.os.IInterface {
-         public static abstract class Stub extends android.os.Binder implements IFoo {
-          }
-          @Override
-          @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
-          public void testMethod();
-          @Override
-          @android.annotation.RequiresNoPermission
-          public void testMethodNoPermission();
-          @Override
-          @android.annotation.PermissionManuallyEnforced
-          public void testMethodManual();
-        }
-        """
-    ).indented()
-
-    // A service with no permission annotation.
-    private val interfaceIBar: TestFile = java(
-        """
-        public interface IBar extends android.os.IInterface {
-         public static abstract class Stub extends android.os.Binder implements IBar {
-          }
-          public void testMethod();
-        }
-        """
-    ).indented()
-
-    // A service whose AIDL Interface is exempted.
-    private val interfaceIExempted: TestFile = java(
-        """
-        package android.accessibilityservice;
-        public interface IBrailleDisplayConnection extends android.os.IInterface {
-         public static abstract class Stub extends android.os.Binder implements IBrailleDisplayConnection {
-          }
-          public void testMethod();
-        }
-        """
-    ).indented()
-
     private val stubs = arrayOf(interfaceIFoo, interfaceIBar, interfaceIExempted)
 
     private fun createVisitedPath(filename: String) =
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetectorTest.kt
new file mode 100644
index 0000000..a33b48c
--- /dev/null
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleRequiresNoPermissionDetectorTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+class SimpleRequiresNoPermissionDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = SimpleRequiresNoPermissionDetector()
+    override fun getIssues(): List<Issue> = listOf(
+        SimpleRequiresNoPermissionDetector
+            .ISSUE_SIMPLE_REQUIRES_NO_PERMISSION
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk()
+
+    fun testRequiresNoPermissionUsedCorrectly_shouldNotWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Foo.java"),
+                    """
+                        package com.android.server;
+                        public class Foo extends IFoo.Stub {
+                            private int memberInt;
+
+                            @Override
+                            @android.annotation.RequiresNoPermission
+                            public void testMethodNoPermission(int parameter1, int parameter2) {
+                                if (parameter1 < parameter2) {
+                                    memberInt = parameter1;
+                                } else {
+                                    memberInt = parameter2;
+                                }
+                            }
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    fun testMissingRequiresNoPermission_shouldWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Bar.java"),
+                    """
+                        package com.android.server;
+                        public class Bar extends IBar.Stub {
+                            private int memberInt;
+
+                            @Override
+                            public void testMethod(int parameter1, int parameter2) {
+                                if (parameter1 < parameter2) {
+                                    memberInt = parameter1;
+                                } else {
+                                    memberInt = parameter2;
+                                }
+                            }
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expect(
+                """
+                src/frameworks/base/services/java/com/android/server/Bar.java:5: Error: Method testMethod doesn't perform any permission checks, meaning it should be annotated with @RequiresNoPermission. [SimpleRequiresNoPermission]
+                    @Override
+                    ^
+                1 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testMethodOnlyPerformsConstructorCall_shouldWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Bar.java"),
+                    """
+                        package com.android.server;
+                        public class Bar extends IBar.Stub {
+                            private IntPair memberIntPair;
+
+                            @Override
+                            public void testMethod(int parameter1, int parameter2) {
+                                memberIntPair = new IntPair(parameter1, parameter2);
+                            }
+
+                            private static class IntPair {
+                                public int first;
+                                public int second;
+
+                                public IntPair(int first, int second) {
+                                    this.first = first;
+                                    this.second = second;
+                                }
+                            }
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expect(
+                """
+                src/frameworks/base/services/java/com/android/server/Bar.java:5: Error: Method testMethod doesn't perform any permission checks, meaning it should be annotated with @RequiresNoPermission. [SimpleRequiresNoPermission]
+                    @Override
+                    ^
+                1 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testMissingRequiresNoPermissionInIgnoredDirectory_shouldNotWarn() {
+        lint()
+            .files(
+                java(
+                    ignoredPath,
+                    """
+                        package com.android.server;
+                        public class Bar extends IBar.Stub {
+                            @Override
+                            public void testMethod(int parameter1, int parameter2) {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    fun testMissingRequiresNoPermissionAbstractMethod_shouldNotWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Bar.java"),
+                    """
+                        package com.android.server;
+                        public abstract class Bar extends IBar.Stub {
+                            private int memberInt;
+
+                            @Override
+                            public abstract void testMethodNoPermission(int parameter1, int parameter2);
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    // If this test fails, consider the following steps:
+    //   1. Pick the first entry (interface) from `exemptAidlInterfaces`.
+    //   2. Change `interfaceIExempted` to use that interface.
+    //   3. Change this test's class to extend the interface's Stub.
+    fun testMissingRequiresNoPermissionAidlInterfaceExempted_shouldNotWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Bar.java"),
+                    """
+                        package com.android.server;
+                        public class Bar extends android.accessibilityservice.IBrailleDisplayConnection.Stub {
+                            public void testMethod(int parameter1, int parameter2) {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    fun testMethodMakesAnotherMethodCall_shouldNotWarn() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("Bar.java"),
+                    """
+                        package com.android.server;
+                        public class Bar extends IBar.Stub {
+                            private int memberInt;
+
+                            @Override
+                            public void testMethod(int parameter1, int parameter2) {
+                                if (!hasPermission()) return;
+
+                                if (parameter1 < parameter2) {
+                                    memberInt = parameter1;
+                                } else {
+                                    memberInt = parameter2;
+                                }
+                            }
+
+                            private bool hasPermission() {
+                                // Perform a permission check.
+                                return true;
+                            }
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    private val stubs = arrayOf(interfaceIFoo, interfaceIBar, interfaceIExempted)
+
+    private fun createVisitedPath(filename: String) =
+        "src/frameworks/base/services/java/com/android/server/$filename"
+
+    private val ignoredPath = "src/test/pkg/TestClass.java"
+}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
index 2ec8fdd..18a8f18 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
@@ -85,4 +85,46 @@
         }
     }
     """.trimIndent()
-)
\ No newline at end of file
+)
+
+// A service with permission annotation on the method.
+val interfaceIFoo: TestFile = java(
+    """
+        public interface IFoo extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IFoo {
+          }
+          @Override
+          @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+          public void testMethod();
+          @Override
+          @android.annotation.RequiresNoPermission
+          public void testMethodNoPermission(int parameter1, int parameter2);
+          @Override
+          @android.annotation.PermissionManuallyEnforced
+          public void testMethodManual();
+        }
+        """
+).indented()
+
+// A service with no permission annotation.
+val interfaceIBar: TestFile = java(
+    """
+        public interface IBar extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IBar {
+          }
+          public void testMethod(int parameter1, int parameter2);
+        }
+        """
+).indented()
+
+// A service whose AIDL Interface is exempted.
+val interfaceIExempted: TestFile = java(
+    """
+        package android.accessibilityservice;
+        public interface IBrailleDisplayConnection extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IBrailleDisplayConnection {
+          }
+          public void testMethod();
+        }
+        """
+).indented()