CVE-2026-84356
Overview
Background
- Exclusive Access Bubble
- the on-screen notice (“Press Esc to exit fullscreen”, etc.) that Chrome shows when a page enters fullscreen/pointer-lock so the user knows an untrusted page controls the whole screen.
- Snackbar / `SnackbarManager` / `SnackbarCollection`
- Android’s transient bottom-of-screen notification widget, its queue manager, and the data structure that holds queued snackbars; on Android the exclusive-access notice is rendered as a high-priority snackbar.
- `setHighPriority` / `TYPE_ACTION`
- a snackbar flag that shields security-critical notices from being discarded by other snackbars’ timeouts;
TYPE_ACTIONsnackbars are the short-lived, user-action snackbars that share a queue. - Snooze / `OnUserInput`
- the mechanism whereby user interaction resets (“snoozes”) the bubble’s hide timer instead of re-showing it, so the notice is not spammed on every keystroke.
Root Cause Analysis
On Android the exclusive-access notice’s lifetime was governed by a native timer whose expiry called ExclusiveAccessBubbleAndroid::Hide(), which unconditionally set was_shown_ = false and told the bridge to hide the snackbar even when the notice had been queued behind, or was not yet visible under, other UI — so the security notice could be dismissed before the user ever saw it. Compounding this, OnUserInput re-snoozed the bubble’s hide timer on every interaction with no bound, letting a page that keeps generating user-input-like activity indefinitely suppress the notice, and ordinary (non-high-priority) snackbars could crowd out or time out the notice in the shared SnackbarCollection queue.
The patch inverts the timer model: Hide() becomes a deliberate no-op that ignores the native timer dismissal, HideImmediately() (now also called from the destructor) performs the real teardown, and the notice’s timeout is moved Java-side (EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS = 3800) so the countdown only starts once the snackbar is actually visible. To defeat suppression-by-interaction, OnExclusiveAccessUserInput now counts snooze resets and, once snooze_reset_count_ reaches kMaxSnoozeResets (10), forces a re-show by setting force_params.force_update = true to bypass the was_shown_ latch. Finally SnackbarCollection gives high-priority security snackbars an isolated “VIP” slot (mActiveHighPriority) that always wins getCurrent(), dedupes rapid duplicates, and enforces a queue cap so normal snackbars cannot starve or evict the notice, and the disclosure/privacy-sandbox notices are marked setHighPriority(true).
Hide() a no-op, times the notice Java-side only while visible, caps snooze resets to force a periodic re-show, and isolates high-priority notices in the snackbar queue.Attack Path
- Enter fullscreen
A malicious page calls
requestFullscreen(or acquires pointer lock), causing Chrome to queue the exclusive-access notice snackbar. - Suppress the notice
The page either spams snackbar-generating actions to crowd the shared queue, rapidly oscillates fullscreen requests, or drives repeated interaction so the pre-patch
Hide()/native timer dismisses the notice orOnUserInputkeeps re-snoozing it, so the “press Esc to exit” warning never becomes visible. - Spoof trusted UI With no visible fullscreen notice, the page paints fake browser chrome/address bar or a spoofed OS/security prompt over the full screen, and the user cannot tell they are in an untrusted fullscreen surface (UI misrepresentation).
- Mitigation forces the notice
After the patch the Java-side duration only counts while the notice is visible, the high-priority snackbar cannot be evicted by normal snackbars, and after
kMaxSnoozeResetsinteractionsforce_updateforces a re-show, so the warning re-appears despite suppression attempts.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java |
modified | |
ifchrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc |
modified | |
SnackbarCollectionchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java |
modified | |
ifchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java |
modified |
Files Changed
components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/text/TemplatePreservingTextView.javachrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.javachrome/browser/ui/android/exclusive_access/exclusive_access_context_android.hchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarManager.javachrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.ccchrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.javatools/metrics/histograms/metadata/ui/enums.xmltools/metrics/histograms/metadata/ui/histograms.xmlchrome/browser/ui/android/exclusive_access/exclusive_access_context_android.ccchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollectionUnitTest.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.javachrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.ccchrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.javachrome/browser/ui/exclusive_access/exclusive_access_bubble.hchrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.javachrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.javachrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarView.java
Audit Directions
- Security notice lifetime tied to timers that run while hiddenReview any warning/notice whose auto-dismiss timer starts before the notice is confirmed visible, or that can be dismissed while occluded/queued.
- Unbounded snooze/reset on user inputLook for handlers that reset a hide timer on every interaction with no cap, allowing a hostile page to suppress a mandatory notice indefinitely; ensure a forced re-show bound exists.
- Shared notification queues mixing security and ordinary itemsAudit queue managers where high-priority/security items share ordering, timeout, or capacity with normal items and can be evicted, timed out, or crowded out.
Patch
From 3e4d359b7a4de442ecd9a5ecb2e8af37639d7568 Mon Sep 17 00:00:00 2001
From: Vikram Pasupathy <vpasupathy@chromium.org>
Date: Thu, 11 Jun 2026 14:22:00 -0700
Subject: [PATCH] Android: Harden Snackbar system against security notice suppression
Implement structural improvements to Android's Snackbar and Exclusive
Access systems to prevent security notice bypasses and UI spoofing.
Design: http://shortn/_FiA9a66mBh
Bug: 517091927, 503787232, 514072194
Change-Id: Ie73529d96023e60fcaa9fb9ea28fafd73a8adae1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7896882
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Reviewed-by: Foromo Daniel Soromou <koretadaniel@chromium.org>
Commit-Queue: Vikram Pasupathy <vpasupathy@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Reviewed-by: Sirisha Kavuluru <skavuluru@google.com>
Cr-Commit-Position: refs/heads/main@{#1645607}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
index d16bae42..d308fbca 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureInfobar.java
@@ -102,6 +102,7 @@
String action = mResources.getString(R.string.ok);
return Snackbar.make(title, mSnackbarController, type, code)
.setAction(action, null)
+ .setHighPriority(true)
.setDefaultLines(false);
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
index 4e7f159..37cf26c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureSnackbar.java
@@ -61,6 +61,7 @@
return Snackbar.make(title, controller, type, code)
.setAction(action, null)
+ .setHighPriority(true)
.setDuration(DURATION_MS)
.setDefaultLines(false);
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java b/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
index bc2646bf..5509167 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ui/ExclusiveAccessBubble.java
@@ -18,6 +18,8 @@
*/
@NullMarked
public class ExclusiveAccessBubble {
+ private static final int EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS = 3800;
+
private final ExclusiveAccessContext mParentContext;
private @Nullable Snackbar mSnackbar;
private final SnackbarManager.SnackbarController mSnackbarController =
@@ -52,7 +54,10 @@
Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
// The exclusive access notice is security-critical and should not
// be discarded by the timeout of other action snackbars in the queue.
- .setHighPriority(true);
+ .setHighPriority(true)
+ // Use a Java-side timeout so that the timer only starts when
+ // the notice is actually visible to the user.
+ .setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS);
snackbarManager.showSnackbar(mSnackbar);
}
}
@@ -60,7 +65,23 @@
@CalledByNative
public void update(String text) {
if (mText != null && mText.equals(text) && mSnackbar != null) return;
+ SnackbarManager snackbarManager = mParentContext.getSnackbarManager();
+ if (snackbarManager == null) return;
+
mText = text;
+ if (mSnackbar != null) {
+ mSnackbar =
+ Snackbar.make(
+ text,
+ mSnackbarController,
+ Snackbar.TYPE_ACTION,
+ Snackbar.UMA_EXCLUSIVE_ACCESS_BUBBLE)
+ .setHighPriority(true)
+ .setDuration(EXCLUSIVE_ACCESS_SNACKBAR_DURATION_MS);
+ // This will trigger SnackbarManager.updateView() and update the existing view.
+ snackbarManager.showSnackbar(mSnackbar);
+ return;
+ }
hide();
show();
}
diff --git a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
index 05d29df..ca887f1 100644
--- a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
+++ b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSnackbarController.java
@@ -41,6 +41,7 @@
Snackbar.TYPE_PERSISTENT,
Snackbar.UMA_PRIVACY_SANDBOX_PAGE_OPEN)
.setAction(mContext.getString(R.string.more), null)
+ .setHighPriority(true)
.setDefaultLines(false));
}
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
index 3ffe446..dccce5e6 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.cc
@@ -96,12 +96,14 @@
}
ExclusiveAccessBubbleAndroid::~ExclusiveAccessBubbleAndroid() {
- Hide();
+ HideImmediately();
}
void ExclusiveAccessBubbleAndroid::Hide() {
- was_shown_ = false;
- bridge_->Hide();
+ // On Android, the exclusive access notice is managed by a Java-side timer
+ // that only starts once the snackbar is visible. We ignore the native
+ // timer dismissal here to prevent the notice from being hidden while
+ // it is covered by other UI elements.
}
void ExclusiveAccessBubbleAndroid::Show() {
@@ -110,7 +112,8 @@
}
void ExclusiveAccessBubbleAndroid::HideImmediately() {
- Hide();
+ was_shown_ = false;
+ bridge_->Hide();
}
void ExclusiveAccessBubbleAndroid::Update(
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
index 61b736fa..312462a 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.cc
@@ -22,8 +22,15 @@
j_activity_tab_provider));
}
+ExclusiveAccessContextAndroid::ExclusiveAccessContextAndroid() = default;
+
ExclusiveAccessContextAndroid::~ExclusiveAccessContextAndroid() = default;
+void ExclusiveAccessContextAndroid::SetBubbleForTesting( // IN-TEST
+ std::unique_ptr<ExclusiveAccessBubbleAndroid> bubble) {
+ exclusive_access_bubble_ = std::move(bubble);
+}
+
void ExclusiveAccessContextAndroid::Destroy(JNIEnv* env) {
Java_ExclusiveAccessContext_destroy(env, java_context_);
}
@@ -119,6 +126,7 @@
void ExclusiveAccessContextAndroid::DestroyAnyExclusiveAccessBubble() {
exclusive_access_bubble_.reset();
exclusive_access_bubble_destruction_task_id_.reset();
+ snooze_reset_count_ = 0;
}
bool ExclusiveAccessContextAndroid::IsExclusiveAccessBubbleDisplayed() const {
@@ -130,7 +138,20 @@
// re-snooze the bubble's hide timer or re-show it if the snooze period has
// elapsed.
if (exclusive_access_bubble_) {
- exclusive_access_bubble_->OnUserInput();
+ // Prevent a malicious site from indefinitely suppressing the security
+ // notice by repeatedly resetting the snooze timer via interaction.
+ // If the user has interacted many times, we force a re-show regardless.
+ if (++snooze_reset_count_ >= kMaxSnoozeResets) {
+ snooze_reset_count_ = 0;
+ ExclusiveAccessBubbleParams force_params =
+ exclusive_access_bubble_->params();
+ // Set force_update to true to bypass the was_shown_ check and force
+ // the notice to re-show.
+ force_params.force_update = true;
+ exclusive_access_bubble_->Update(force_params, base::NullCallback());
+ } else {
+ exclusive_access_bubble_->OnUserInput();
+ }
}
}
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
index 52e5dc1..c9dc89d 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h
@@ -22,8 +22,14 @@
const jni_zero::JavaRef<jobject>& j_context,
const jni_zero::JavaRef<jobject>& j_fullscreen_manager,
const jni_zero::JavaRef<jobject>& j_activity_tab_provider);
+ // Constructor for testing, bypasses JNI creation.
+ ExclusiveAccessContextAndroid();
~ExclusiveAccessContextAndroid() override;
+ // Injects an exclusive access bubble instance for testing.
+ void SetBubbleForTesting(
+ std::unique_ptr<ExclusiveAccessBubbleAndroid> bubble);
+
void Destroy(JNIEnv* env);
bool IsFullscreen() const override;
@@ -70,11 +76,14 @@
void ForceActiveTab(JNIEnv* env, const jni_zero::JavaRef<jobject>& j_tab);
+ private:
+ int snooze_reset_count_ = 0;
+ static constexpr int kMaxSnoozeResets = 10;
+
base::WeakPtr<ExclusiveAccessContextAndroid> GetAsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
- private:
void DestroyAnyExclusiveAccessBubble();
base::android::ScopedJavaGlobalRef<jobject> java_context_;
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
index 376bae87..6325a826 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_bubble.h
@@ -24,6 +24,9 @@
// Called on user input to update timers and/or re-show the bubble.
void OnUserInput();
+ // Returns the parameters used to configure the bubble.
+ const ExclusiveAccessBubbleParams& params() const { return params_; }
+
// Time the bubble is shown before hiding automatically.
static constexpr base::TimeDelta kShowTime = base::Milliseconds(3800);
// Time without user input that must elapse before the bubble is re-shown.
diff --git a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
index 9099770..72c7d65 100644
--- a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
+++ b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/Snackbar.java
@@ -31,7 +31,7 @@
public class Snackbar {
/**
* Snackbars that are created as an immediate response to user's action. These snackbars are
- * managed in a stack and will be swiped away altogether after timeout.
+ * managed in a stack and will be dismissed one by one.
*/
public static final int TYPE_ACTION = 0;
@@ -205,7 +205,7 @@
/**
* Sets whether the snackbar is high priority. High priority snackbars are shielded from being
* discarded by the timeout of other action-type snackbars in the queue (e.g. for security-
- * critical notices).
+ * critical notices), and they stay at the front of the queue.
*/
public Snackbar setHighPriority(boolean highPriority) {
mIsHighPriority = highPriority;
diff --git a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
index bac0e5bb..bb05f57e 100644
--- a/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
+++ b/chrome/browser/ui/messages/android/java/src/org/chromium/chrome/browser/ui/messages/snackbar/SnackbarCollection.java
@@ -15,87 +15,148 @@
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager.SnackbarController;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
+import java.util.List;
/** A data structure that holds all the {@link Snackbar}s managed by {@link SnackbarManager}. */
@NullMarked
class SnackbarCollection {
+ // Standard FIFO queue for normal notifications (e.g., "Copied to clipboard").
private final Deque<Snackbar> mSnackbars = new ArrayDeque<>();
+ // Queue for persistent notifications (e.g., "Offline mode").
private final Deque<Snackbar> mPersistentSnackbars = new ArrayDeque<>();
+ // Holds a single High Priority security warning (e.g., Fullscreen).
+ // This isolates HP snackbars from the standard queues, allowing O(1) interruption.
+ private @Nullable Snackbar mActiveHighPriority;
- /**
- * Adds a new snackbar to the collection. If the new snackbar is of
- * {@link Snackbar#TYPE_ACTION} and current snackbar is of
- * {@link Snackbar#TYPE_NOTIFICATION}, the current snackbar will be removed from the
- * collection immediately. Snackbars of {@link Snackbar#TYPE_PERSISTENT} will appear after all
- * action and notification type snackbars are dismissed and will be hidden if an action or
- * notifications are added after it.
- */
+ private static final int MAX_SNACKBARS = 10;
+
void add(Snackbar snackbar) {
- if (snackbar.isTypeAction()) {
+ RecordHistogram.recordExactLinearHistogram(
+ "Snackbar.QueueDepthAtInsertion",
+ mSnackbars.size()
+ + mPersistentSnackbars.size()
+ + (mActiveHighPriority != null ? 1 : 0),
+ MAX_SNACKBARS + 1);
+
+ if (snackbar.isHighPriority()) {
+ // High Priority (HP) Security Warnings bypass the regular queues entirely.
+ if (mActiveHighPriority != null) {
+ // State Deduplication: Rapid oscillation attacks (e.g., spamming requestFullscreen)
+ // are collapsed here. If it's the exact same warning, we ignore the duplicate.
+ if (mActiveHighPriority.getController() == snackbar.getController()
+ && objectsAreEqual(
+ mActiveHighPriority.getActionData(), snackbar.getActionData())) {
+ return; // Deduplication: already have this HP snackbar.
+ }
+ // LIFO Immediate Interruption: The new HP warning instantly overwrites the old one.
+ RecordHistogram.recordEnumeratedHistogram(
+ "Snackbar.DismissalReason",
+ DismissalReason.REPLACED_BY_HIGH_PRIORITY,
+ DismissalReason.NUM_ENTRIES);
+ assert mActiveHighPriority.getController() != null;
+ if (mActiveHighPriority.getController() != null) {
+ mActiveHighPriority
+ .getController()
+ .onDismissNoAction(mActiveHighPriority.getActionData());
+ }
+ }
+ // Assign to the VIP slot. getCurrent() will now prioritize this above all else.
+ mActiveHighPriority = snackbar;
+ return;
+ }
+
+ if (snackbar.isTypePersistent()) {
+ assert !TextUtils.isEmpty(snackbar.getActionText())
+ : "Persistent snackbars require action text.";
+ mPersistentSnackbars.addFirst(snackbar);
+ } else if (snackbar.isTypeAction()) {
if (getCurrent() != null && !getCurrent().isTypeAction()) {
removeCurrent(DismissalReason.REPLACED_BY_ACTION_SNACKBAR);
}
mSnackbars.addFirst(snackbar);
- } else if (snackbar.isTypePersistent()) {
- // Although persistent snackbars set their action text by default, it is possible that
- // the developer overrides it. This is a safeguard to ensure all persistent snackbars
- // have a method of dismissal.
- assert !TextUtils.isEmpty(snackbar.getActionText())
- : "Persistent snackbars require action text.";
- mPersistentSnackbars.addFirst(snackbar);
} else {
mSnackbars.addLast(snackbar);
}
+
+ // Enforce a hard capacity limit to prevent queue exhaustion.
+ // Drop the oldest unseen standard notifications from the front of the queue.
+ while (mSnackbars.size() > MAX_SNACKBARS) {
+ removeOldestUnseenSnackbar(mSnackbars);
+ }
+ while (mPersistentSnackbars.size() > MAX_SNACKBARS) {
+ removeOldestUnseenSnackbar(mPersistentSnackbars);
+ }
}
- /**
- * Removes the current snackbar from the collection after the user has clicked on the action
- * button.
- */
- Snackbar removeCurrentDueToAction() {
+ private void removeOldestUnseenSnackbar(Deque<Snackbar> list) {
+ Iterator<Snackbar> iter = list.iterator();
+ if (iter.hasNext() && list.peekFirst() == getCurrent()) {
+ iter.next(); // Skip the currently showing one
+ }
+ if (!iter.hasNext()) return;
+ Snackbar toRemove = iter.next();
+ iter.remove();
+ RecordHistogram.recordEnumeratedHistogram(
+ "Snackbar.DismissalReason", DismissalReason.OTHERS, DismissalReason.NUM_ENTRIES);
+ SnackbarController controller = toRemove.getController();
+ if (controller != null) {
+ controller.onDismissNoAction(toRemove.getActionData());
+ }
+ }
+
+ boolean contains(Snackbar snackbar) {
+ return snackbar == mActiveHighPriority
+ || mSnackbars.contains(snackbar)
+ || mPersistentSnackbars.contains(snackbar);
+ }
+
+ @Nullable Snackbar getCurrent() {
+ if (mActiveHighPriority != null) return mActiveHighPriority;
+ Snackbar actionCurrent = mSnackbars.peekFirst();
+ Snackbar persistentCurrent = mPersistentSnackbars.peekFirst();
+ return actionCurrent != null ? actionCurrent : persistentCurrent;
+ }
+
+ @Nullable Snackbar removeCurrentDueToAction() {
return removeCurrent(DismissalReason.ACTION_BUTTON);
}
- /** Removes the current snackbar from the collection after the user has swiped it away. */
@Nullable Snackbar removeCurrentDueToSwipe() {
return removeCurrent(DismissalReason.SWIPE);
}
... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
index c29f21816c..976f848 100644
--- a/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
+++ b/chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android_unittest.cc
@@ -4,8 +4,13 @@
#include "chrome/browser/ui/android/exclusive_access/exclusive_access_bubble_android.h"
+#include <memory>
+#include <string>
+#include <utility>
+
#include "base/android/jni_string.h"
#include "base/functional/callback_helpers.h"
+#include "chrome/browser/ui/android/exclusive_access/exclusive_access_context_android.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -106,4 +111,45 @@
EXPECT_CALL(*mock_bridge_ptr, Hide()).Times(1);
}
+TEST_F(ExclusiveAccessBubbleAndroidTest, SnoozeResetForciblyReshowsNotice) {
+ ExclusiveAccessBubbleParams params;
+ params.type = EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
+
+ auto mock_bridge = std::make_unique<MockBridge>();
+ auto* mock_bridge_ptr = mock_bridge.get();
+
+ // Initial show on creation.
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, Update(_)).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+
+ auto bubble = std::make_unique<ExclusiveAccessBubbleAndroid>(
+ params, base::DoNothing(), std::move(mock_bridge));
+
+ testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
+
+ ExclusiveAccessContextAndroid context;
+ context.SetBubbleForTesting(std::move(bubble));
+
+ // Verify that the first 9 user inputs don't trigger any show or update on the
+ // bridge.
+ for (int i = 1; i <= 9; ++i) {
+ context.OnExclusiveAccessUserInput();
+ }
+
+ // The 10th user input exceeds the snooze interaction threshold and must
+ // forcibly re-show the security notice regardless of whether it was
+ // previously shown in this session (i.e. force_update is set to true to
+ // override the was_shown_ latch).
+ EXPECT_CALL(*mock_bridge_ptr, IsVisible()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, IsKeyboardConnected()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_bridge_ptr, Update(_)).Times(1);
+ EXPECT_CALL(*mock_bridge_ptr, Show()).Times(1);
+
+ context.OnExclusiveAccessUserInput();
+
+ testing::Mock::VerifyAndClearExpectations(mock_bridge_ptr);
+}
+
} // namespace