diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java index 256b749b87535..65ce8ec649da1 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java @@ -45,6 +45,7 @@ import org.chromium.base.metrics.RecordUserAction; import org.chromium.base.supplier.OneshotSupplier; import org.chromium.base.task.PostTask; import org.chromium.base.task.TaskTraits; +import org.chromium.blink.mojom.DisplayMode; import org.chromium.chrome.R; import org.chromium.chrome.browser.DeferredStartupHandler; import org.chromium.chrome.browser.KeyboardShortcuts; @@ -618,7 +619,9 @@ public abstract class BaseCustomTabActivity extends ChromeActivity { InstalledWebappDataRegister.prefetchPreferences(); if (ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled() - && mIntentDataProvider.isWebappOrWebApkActivity()) { + && mIntentDataProvider.isWebappOrWebApkActivity() + && (mIntentDataProvider.getResolvedDisplayMode() != DisplayMode.STANDALONE + || ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.getValue())) { // The window's edge-to-edge state is owned by token holders (display cutout // controller, immersive mode). While any token is held, withhold system bar and // display cutout insets from the edge-to-edge root layout so the web app content diff --git a/chrome/android/java/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelper.java index dcc6b4b13c15b..73a4676a3d89b 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelper.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelper.java @@ -146,7 +146,9 @@ public class DisplayCutoutTabHelper implements UserData { @Override public boolean isShortEdgesCutoutModeEnabled() { - return ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled(); + return ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled() + && (getDisplayMode() != DisplayMode.STANDALONE + || ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.getValue()); } /** diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappActivity.java index 3c45b1ad8638f..97170795bc771 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappActivity.java @@ -18,6 +18,7 @@ import androidx.browser.customtabs.CustomTabsIntent; import org.chromium.base.IntentUtils; import org.chromium.base.ResettersForTesting; import org.chromium.base.metrics.RecordUserAction; +import org.chromium.blink.mojom.DisplayMode; import org.chromium.build.annotations.NullMarked; import org.chromium.build.annotations.Nullable; import org.chromium.chrome.R; @@ -38,10 +39,16 @@ public class WebappActivity extends BaseCustomTabActivity { public static final String WEBAPP_SCHEME = "webapp"; private static @Nullable BrowserServicesIntentDataProvider sIntentDataProviderForTesting; + private @Nullable BrowserServicesIntentDataProvider mStartupIntentDataProvider; @Override protected @Nullable BrowserServicesIntentDataProvider buildIntentDataProvider( Intent intent, @CustomTabsIntent.ColorScheme int colorScheme) { + if (mStartupIntentDataProvider != null) { + BrowserServicesIntentDataProvider provider = mStartupIntentDataProvider; + mStartupIntentDataProvider = null; + return provider; + } if (intent == null) return null; if (sIntentDataProviderForTesting != null) { @@ -73,25 +80,35 @@ public class WebappActivity extends BaseCustomTabActivity { ResettersForTesting.register(() -> sIntentDataProviderForTesting = null); } - // When sWebAppShortEdgesCutoutMode is enabled, intentionally skip the activity-level - // edge-to-edge token at creation time and let DisplayCutoutController acquire it later, - // only after the page declares viewport-fit=cover. Drawing edge-to-edge unconditionally on - // create would push standalone PWAs under the status bar even when the page never opted in. + private boolean isShortEdgesCutoutModeEnabledForApp() { + if (!ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled()) return false; + BrowserServicesIntentDataProvider provider = getIntentDataProvider(); + if (provider == null) { + if (mStartupIntentDataProvider == null) { + mStartupIntentDataProvider = + buildIntentDataProvider(getIntent(), CustomTabsIntent.COLOR_SCHEME_LIGHT); + } + provider = mStartupIntentDataProvider; + } + return provider != null + && (provider.getResolvedDisplayMode() != DisplayMode.STANDALONE + || ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.getValue()); + } + @Override protected boolean shouldDrawEdgeToEdgeOnCreate() { - return !ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled() - && super.shouldDrawEdgeToEdgeOnCreate(); + return !isShortEdgesCutoutModeEnabledForApp() && super.shouldDrawEdgeToEdgeOnCreate(); } @Override protected boolean canColorStatusBarWithEdgeToEdgeHelper() { - return ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled() + return isShortEdgesCutoutModeEnabledForApp() || super.canColorStatusBarWithEdgeToEdgeHelper(); } @Override protected boolean canSetTransparentStatusBarWithoutDelegate() { - return ChromeFeatureList.sWebAppShortEdgesCutoutMode.isEnabled(); + return isShortEdgesCutoutModeEnabledForApp(); } @Override diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelperTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelperTest.java index 5b751b5384ccd..b608eed948588 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelperTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/display_cutout/DisplayCutoutTabHelperTest.java @@ -4,6 +4,8 @@ package org.chromium.chrome.browser.display_cutout; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -18,7 +20,11 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.Features.EnableFeatures; +import org.chromium.blink.mojom.DisplayMode; +import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider; import org.chromium.chrome.browser.customtabs.BaseCustomTabActivity; +import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.tab.Tab; import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.edge_to_edge.EdgeToEdgeManager; @@ -42,6 +48,7 @@ public class DisplayCutoutTabHelperTest { @Mock private EdgeToEdgeManager mManagerB; @Mock private EdgeToEdgeStateProvider mProviderA; @Mock private EdgeToEdgeStateProvider mProviderB; + @Mock private BrowserServicesIntentDataProvider mIntentDataProvider; @Before public void setUp() { @@ -59,6 +66,32 @@ public class DisplayCutoutTabHelperTest { .thenReturn(new WeakReference<>(mNonCustomTabActivity)); } + @Test + @EnableFeatures(ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE) + public void testStandaloneShortEdgesRequiresParam() { + when(mTab.getWindowAndroid()).thenReturn(mWindowAndroidA); + when(mActivityA.getIntentDataProvider()).thenReturn(mIntentDataProvider); + when(mIntentDataProvider.getResolvedDisplayMode()).thenReturn(DisplayMode.STANDALONE); + DisplayCutoutTabHelper.ChromeDisplayCutoutDelegate delegate = + new DisplayCutoutTabHelper.ChromeDisplayCutoutDelegate(mTab); + + assertFalse(delegate.isShortEdgesCutoutModeEnabled()); + ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.setForTesting(true); + assertTrue(delegate.isShortEdgesCutoutModeEnabled()); + } + + @Test + @EnableFeatures(ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE) + public void testFullscreenShortEdgesDoesNotRequireParam() { + when(mTab.getWindowAndroid()).thenReturn(mWindowAndroidA); + when(mActivityA.getIntentDataProvider()).thenReturn(mIntentDataProvider); + when(mIntentDataProvider.getResolvedDisplayMode()).thenReturn(DisplayMode.FULLSCREEN); + DisplayCutoutTabHelper.ChromeDisplayCutoutDelegate delegate = + new DisplayCutoutTabHelper.ChromeDisplayCutoutDelegate(mTab); + + assertTrue(delegate.isShortEdgesCutoutModeEnabled()); + } + @Test public void testEdgeToEdgeTokenRebindsAfterActivityReparenting() { when(mTab.getWindowAndroid()).thenReturn(mWindowAndroidA); diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappActivityTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappActivityTest.java index 32d2c4f04d98e..9b84270e638a5 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappActivityTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappActivityTest.java @@ -7,6 +7,7 @@ package org.chromium.chrome.browser.webapps; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -42,6 +43,11 @@ import org.chromium.webapk.test.WebApkTestHelper; public class WebappActivityTest { private static class TestWebappActivity extends WebappActivity { private @Nullable BrowserServicesIntentDataProvider mMockIntentDataProvider; + private @Nullable BrowserServicesIntentDataProvider mStartupProviderForTesting; + + void setStartupProviderForTesting(BrowserServicesIntentDataProvider provider) { + mStartupProviderForTesting = provider; + } void setMockIntentDataProvider(@Nullable BrowserServicesIntentDataProvider provider) { mMockIntentDataProvider = provider; @@ -54,6 +60,13 @@ public class WebappActivityTest { : super.getIntentDataProvider(); } + @Override + protected @Nullable BrowserServicesIntentDataProvider buildIntentDataProvider( + Intent intent, @CustomTabsIntent.ColorScheme int colorScheme) { + if (mStartupProviderForTesting != null) return mStartupProviderForTesting; + return super.buildIntentDataProvider(intent, colorScheme); + } + BrowserServicesIntentDataProvider callBuildIntentDataProvider(Intent intent) { return buildIntentDataProvider(intent, CustomTabsIntent.COLOR_SCHEME_LIGHT); } @@ -132,9 +145,105 @@ public class WebappActivityTest { }) public void shouldNotDrawEdgeToEdgeOnCreateWithShortEdgesEnabled() { TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.STANDALONE); + activity.setMockIntentDataProvider(provider); - assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); + assertTrue(activity.callShouldDrawEdgeToEdgeOnCreate()); assertTrue(activity.callCanColorStatusBarWithEdgeToEdgeHelper()); + assertFalse(activity.callCanSetTransparentStatusBarWithoutDelegate()); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void standaloneOptInUsesShortEdges() { + ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.setForTesting(true); + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.STANDALONE); + activity.setMockIntentDataProvider(provider); + + assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); + assertTrue(activity.callCanSetTransparentStatusBarWithoutDelegate()); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void standaloneStartupBeforeIntentProviderExistsDoesNotAcquireActivityToken() { + ChromeFeatureList.sWebAppShortEdgesCutoutModeStandalone.setForTesting(true); + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.STANDALONE); + activity.setStartupProviderForTesting(provider); + activity.setIntent(new Intent()); + + assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void standaloneStartupWithParamOffKeepsLegacyActivityToken() { + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.STANDALONE); + activity.setStartupProviderForTesting(provider); + activity.setIntent(new Intent()); + + assertTrue(activity.callShouldDrawEdgeToEdgeOnCreate()); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void fullscreenStartupBeforeIntentProviderExistsDoesNotAcquireActivityToken() { + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.FULLSCREEN); + activity.setStartupProviderForTesting(provider); + activity.setIntent(new Intent()); + + assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void startupProviderIsReusedDuringPreInflation() { + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.FULLSCREEN); + activity.setIntent(new Intent()); + WebappActivity.setIntentDataProviderForTesting(provider); + + assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); + assertSame(provider, activity.callBuildIntentDataProvider(activity.getIntent())); + } + + @Test + @Features.EnableFeatures({ + ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE, + ChromeFeatureList.WEB_APP_SHORT_EDGES_CUTOUT_MODE + }) + public void fullscreenStillUsesShortEdgesByDefault() { + TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.FULLSCREEN); + activity.setMockIntentDataProvider(provider); + + assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); assertTrue(activity.callCanSetTransparentStatusBarWithoutDelegate()); } @@ -143,6 +252,9 @@ public class WebappActivityTest { @Features.DisableFeatures(ChromeFeatureList.EDGE_TO_EDGE_EVERYWHERE) public void shortEdgesCanColorStatusBarWithoutDelegate() { TestWebappActivity activity = new TestWebappActivity(); + BrowserServicesIntentDataProvider provider = mock(BrowserServicesIntentDataProvider.class); + when(provider.getResolvedDisplayMode()).thenReturn(DisplayMode.FULLSCREEN); + activity.setMockIntentDataProvider(provider); assertFalse(activity.callShouldDrawEdgeToEdgeOnCreate()); assertTrue(activity.callCanColorStatusBarWithEdgeToEdgeHelper()); diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json index a0102b393ccc8..6fc8ce609442c 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json @@ -10262,7 +10262,7 @@ { "name": "web-app-short-edges-cutout-mode", "owners": [ "pwa-team@google.com", "helmut@januschka.com" ], - "expiry_milestone": 156 + "expiry_milestone": 157 }, { "name": "web-authentication-ambient-signin", diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h index c5d9ffef4315f..909a0821cf17f 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -6140,11 +6140,9 @@ inline constexpr char kEdgeToEdgeExtraLogsDescription[] = inline constexpr char kWebAppShortEdgesCutoutModeName[] = "Web App Short Edges Cutout Mode"; inline constexpr char kWebAppShortEdgesCutoutModeDescription[] = - "When enabled, installed PWAs (display: standalone or display: fullscreen) " - "that opt in via viewport-fit=cover draw under the status and navigation " - "bars on Android, with non-zero CSS env(safe-area-inset-*) values for " - "controls. Standalone webapps that do not declare viewport-fit=cover are " - "unaffected."; + "When enabled, installed fullscreen PWAs can draw into display cutouts " + "on Android. Standalone PWAs require the enable_standalone feature " + "parameter to opt in to edge-to-edge with viewport-fit=cover."; inline constexpr char kEnableAccessibilityLabeledByName[] = "Enable Accessibility LabeledBy"; diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java index 510dcc6d638f8..50f456e6b119b 100644 --- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java +++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java @@ -2370,6 +2370,10 @@ public abstract class ChromeFeatureList { newIntCachedFeatureParam( ANDROID_TIPS_NOTIFICATIONS_V2, "max_tips_opt_in_promo_show_count", 2); + public static final BooleanCachedFeatureParam sWebAppShortEdgesCutoutModeStandalone = + newBooleanCachedFeatureParam( + WEB_APP_SHORT_EDGES_CUTOUT_MODE, "enable_standalone", false); + /** All {@link CachedFeatureParam}s of features in this FeatureList */ public static final List> sParamsCached = List.of( @@ -2465,7 +2469,8 @@ public abstract class ChromeFeatureList { sTouchToSearchCalloutIph, sTouchToSearchCalloutSnippetAsSubtitle, sUseChimeAndroidSdkAlwaysRegister, - sWebApkMinShellApkVersionValue + sWebApkMinShellApkVersionValue, + sWebAppShortEdgesCutoutModeStandalone // keep-sorted end ); diff --git a/components/webapps/docs/android_architecture.md b/components/webapps/docs/android_architecture.md index fa117c43748bb..f47fd1a22935e 100644 --- a/components/webapps/docs/android_architecture.md +++ b/components/webapps/docs/android_architecture.md @@ -462,10 +462,11 @@ There are two paths that enable drawing into the cutout area: `viewport-fit` value. - **Non-immersive edge-to-edge, deferred to `DisplayCutoutController`** (behind - the `WebAppShortEdgesCutoutMode` feature): + the `WebAppShortEdgesCutoutMode` feature and its default-off + `enable_standalone` parameter): - - With the feature enabled, `display: standalone` webapps no longer draw - edge-to-edge unconditionally at activity creation: + - With the feature and parameter enabled, `display: standalone` webapps no + longer draw edge-to-edge unconditionally at activity creation: `WebappActivity#shouldDrawEdgeToEdgeOnCreate` returns false, so the activity starts with the default (non-edge-to-edge) window layout. - Instead, `DisplayCutoutController` takes over once the page's `viewport-fit` @@ -476,8 +477,9 @@ There are two paths that enable drawing into the cutout area: - The controller also reacts to dynamic changes: if a page updates its `viewport-fit` meta tag via JavaScript after load, the window layout is updated accordingly. - - With the feature disabled, standalone webapps keep the legacy behavior of - drawing edge-to-edge on creation regardless of `viewport-fit`. + - With the feature disabled or `enable_standalone` false, standalone webapps + keep the legacy behavior of drawing edge-to-edge on creation regardless of + `viewport-fit` when edge-to-edge everywhere is enabled. ### TWA Launch Parameters