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()