Skip to content

Commit 46b3c5f

Browse files
Nicolas Dossou-GbeteChromium LUCI CQ
authored andcommitted
[M130] clay: Optimistically show the dialog and unblock after timeout
We expect that there will be very few cases where a client is eligible for the dialog but for which the choice is not required. So we flip the triggering logic to show the dialog if eligible, as a better way to manage the delay until we get a response from the service that will confirm that blocking is required. That delay is limited to a max duration specified via the "dialog_timeout_millis" Finch feature param. Note: depends on the internal https://crrev.com/i/7688982 (cherry picked from commit dabf53d) Fixed: b:365926620 Fixed: 367915819 Change-Id: I0b069c47995ba721cee98327b7c7315e05edcf37 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5857956 Code-Coverage: [email protected] <[email protected]> Commit-Queue: Nicolas Dossou-Gbété <[email protected]> Reviewed-by: Boris Sazonov <[email protected]> Mega-CQ: Nicolas Dossou-Gbété <[email protected]> Cr-Original-Commit-Position: refs/heads/main@{#1356399} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5872912 Cr-Commit-Position: refs/branch-heads/6723@{#80} Cr-Branched-From: 985f296-refs/heads/main@{#1356013}
1 parent 78dfa60 commit 46b3c5f

File tree

10 files changed

+421
-122
lines changed

10 files changed

+421
-122
lines changed

chrome/browser/search_engines/android/java/src/org/chromium/chrome/browser/search_engines/choice_screen/ChoiceDialogCoordinator.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import androidx.activity.OnBackPressedCallback;
1414
import androidx.annotation.NonNull;
15+
import androidx.annotation.Nullable;
1516
import androidx.annotation.VisibleForTesting;
1617

1718
import org.chromium.base.Callback;
@@ -39,7 +40,7 @@ interface ViewHolder {
3940
View getView();
4041

4142
void updateViewForType(
42-
@DialogType int dialogType, Callback<Integer> actionButtonClickCallback);
43+
@DialogType int dialogType, @Nullable Callback<Integer> actionButtonClickCallback);
4344
}
4445

4546
private final ViewHolder mViewHolder;
@@ -123,10 +124,12 @@ public void onDismiss(
123124

124125
@Override
125126
public void updateDialogType(@DialogType int dialogType) {
126-
mViewHolder.updateViewForType(dialogType, mMediator::onActionButtonClick);
127+
mViewHolder.updateViewForType(
128+
dialogType,
129+
dialogType == DialogType.LOADING ? null : mMediator::onActionButtonClick);
127130

128131
switch (dialogType) {
129-
case DialogType.CHOICE_LAUNCH -> {
132+
case DialogType.LOADING, DialogType.CHOICE_LAUNCH -> {
130133
mModel.set(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, false);
131134
mModel.set(
132135
ModalDialogProperties.APP_MODAL_DIALOG_BACK_PRESS_HANDLER,
@@ -176,14 +179,14 @@ public View getView() {
176179

177180
@Override
178181
public void updateViewForType(
179-
@DialogType int dialogType, Callback<Integer> actionButtonClickCallback) {
182+
@DialogType int dialogType, @Nullable Callback<Integer> actionButtonClickCallback) {
180183
View illustration = mView.findViewById(R.id.illustration);
181184
TextView title = mView.findViewById(R.id.choice_dialog_title);
182185
TextView message = mView.findViewById(R.id.choice_dialog_message);
183186
ButtonCompat button = mView.findViewById(R.id.choice_dialog_button);
184187

185188
switch (dialogType) {
186-
case DialogType.CHOICE_LAUNCH -> {
189+
case DialogType.LOADING, DialogType.CHOICE_LAUNCH -> {
187190
illustration.setBackgroundResource(
188191
R.drawable.blocking_choice_dialog_illustration);
189192
title.setText(R.string.blocking_choice_dialog_first_title);
@@ -200,7 +203,11 @@ public void updateViewForType(
200203
case DialogType.UNKNOWN -> throw new IllegalStateException();
201204
}
202205

203-
button.setOnClickListener(ignored -> actionButtonClickCallback.onResult(dialogType));
206+
button.setOnClickListener(
207+
actionButtonClickCallback == null
208+
? null
209+
: ignored -> actionButtonClickCallback.onResult(dialogType));
210+
button.setEnabled(actionButtonClickCallback != null);
204211
}
205212
}
206213
}

chrome/browser/search_engines/android/java/src/org/chromium/chrome/browser/search_engines/choice_screen/ChoiceDialogMediator.java

Lines changed: 137 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,33 @@
55
package org.chromium.chrome.browser.search_engines.choice_screen;
66

77
import androidx.annotation.IntDef;
8+
import androidx.annotation.MainThread;
89
import androidx.annotation.NonNull;
910
import androidx.annotation.Nullable;
1011

1112
import org.chromium.base.Callback;
1213
import org.chromium.base.Log;
14+
import org.chromium.base.ThreadUtils;
1315
import org.chromium.base.supplier.ObservableSupplier;
1416
import org.chromium.components.search_engines.SearchEngineChoiceService;
17+
import org.chromium.components.search_engines.SearchEnginesFeatureUtils;
1518

1619
import java.lang.annotation.Retention;
1720
import java.lang.annotation.RetentionPolicy;
1821

1922
class ChoiceDialogMediator {
20-
@IntDef({DialogType.UNKNOWN, DialogType.CHOICE_LAUNCH, DialogType.CHOICE_CONFIRM})
23+
@IntDef({
24+
DialogType.UNKNOWN,
25+
DialogType.LOADING,
26+
DialogType.CHOICE_LAUNCH,
27+
DialogType.CHOICE_CONFIRM
28+
})
2129
@Retention(RetentionPolicy.SOURCE)
2230
@interface DialogType {
2331
int UNKNOWN = 0;
24-
int CHOICE_LAUNCH = 1;
25-
int CHOICE_CONFIRM = 2;
32+
int LOADING = 1;
33+
int CHOICE_LAUNCH = 2;
34+
int CHOICE_CONFIRM = 3;
2635
}
2736

2837
/** See {@link #startObserving}. */
@@ -50,7 +59,25 @@ interface Delegate {
5059
private final Callback<Boolean> mIsDeviceChoiceRequiredObserver;
5160

5261
private @DialogType int mDialogType = DialogType.UNKNOWN;
53-
private boolean mIsDialogShown;
62+
63+
/**
64+
* Either the time at which the blocking dialog was shown, {@code null} indicating that the
65+
* dialog was not shown yet, or {@link Long#MIN_VALUE} indicating that the dialog has been
66+
* dismissed.
67+
*/
68+
private @Nullable Long mDialogAddedTimeMillis;
69+
70+
/**
71+
* Either the time at which observing the service started, or {@code null} if it didn't happen
72+
* yet.
73+
*/
74+
private @Nullable Long mObservationStartedTimeMillis;
75+
76+
/**
77+
* Either the time at which the first service event was received, or {@code null} if it didn't
78+
* happen yet.
79+
*/
80+
private @Nullable Long mFirstServiceEventTimeMillis;
5481

5582
private @Nullable Delegate mDelegate;
5683

@@ -81,6 +108,20 @@ void startObserving(@NonNull Delegate delegate) {
81108
assert mDelegate == null;
82109
mDelegate = delegate;
83110

111+
mObservationStartedTimeMillis = System.currentTimeMillis();
112+
mDialogType = DialogType.LOADING;
113+
114+
if (!mIsDeviceChoiceRequiredSupplier.hasValue()) {
115+
// An initial response from the supplier is still pending, so it won't call the observer
116+
// on registration by itself. It's unclear how long it would take. We proactively
117+
// trigger the blocking dialog, but if it takes too long we will unblock the user.
118+
// We do it asynchronously to match how it is done via the supplier when it has a value.
119+
ThreadUtils.postOnUiThread(
120+
() -> {
121+
mDelegate.updateDialogType(DialogType.LOADING);
122+
mDelegate.showDialog();
123+
});
124+
}
84125
mIsDeviceChoiceRequiredSupplier.addObserver(mIsDeviceChoiceRequiredObserver);
85126
}
86127

@@ -108,67 +149,129 @@ void onActionButtonClick(@DialogType int dialogType) {
108149
switch (dialogType) {
109150
case DialogType.CHOICE_LAUNCH -> mSearchEngineChoiceService.launchDeviceChoiceScreens();
110151
case DialogType.CHOICE_CONFIRM -> mDelegate.dismissDialog();
111-
case DialogType.UNKNOWN -> throw new IllegalStateException();
152+
case DialogType.LOADING, DialogType.UNKNOWN -> throw new IllegalStateException();
112153
}
113154
}
114155

115156
/** Method to call when the dialog is actually shown. */
116157
void onDialogAdded() {
117-
mIsDialogShown = true;
158+
assert mDialogAddedTimeMillis == null
159+
: "The dialog is not expected to have already been shown";
160+
assert mDialogType != DialogType.UNKNOWN;
161+
assert mObservationStartedTimeMillis != null;
162+
mDialogAddedTimeMillis = System.currentTimeMillis();
118163
mSearchEngineChoiceService.notifyDeviceChoiceBlockShown();
164+
165+
// TODO(b/355201070): Replace this after e2e testing with UMA recording.
166+
Log.i(
167+
TAG,
168+
"onDialogAdded(), time since observation start: %s millis",
169+
mDialogAddedTimeMillis - mObservationStartedTimeMillis);
170+
scheduleDismissOnDeviceChoiceRequiredUpdateTimeout();
119171
}
120172

121173
void onDialogDismissed() {
122-
mIsDialogShown = false;
123174
destroy();
124175
}
125176

177+
@MainThread
126178
private void onIsDeviceChoiceRequiredChanged(@Nullable Boolean isDeviceChoiceRequired) {
179+
ThreadUtils.checkUiThread();
180+
127181
assert mDelegate != null;
182+
boolean wasDialogShown = mDialogAddedTimeMillis != null;
183+
boolean wasDialogDismissed = wasDialogShown && mDialogType == DialogType.UNKNOWN;
184+
185+
if (mFirstServiceEventTimeMillis == null) {
186+
mFirstServiceEventTimeMillis = System.currentTimeMillis();
187+
// TODO(b/355201070): Replace this after e2e testing with UMA recording.
188+
Log.i(
189+
TAG,
190+
"onIsDeviceChoiceRequiredChanged(%s), time since dialog added: %s millis, "
191+
+ "time since observation started: %s millis",
192+
isDeviceChoiceRequired,
193+
wasDialogShown
194+
? mFirstServiceEventTimeMillis - mDialogAddedTimeMillis
195+
: "<N/A>",
196+
mObservationStartedTimeMillis != null
197+
? mFirstServiceEventTimeMillis - mObservationStartedTimeMillis
198+
: "<N/A>");
199+
}
128200

129-
if (Boolean.TRUE.equals(isDeviceChoiceRequired)) {
130-
// We expect it to happen only as the very first notification we get. Other values as
131-
// first notification lead to skipping the dialog entirely.
132-
assert !mIsDialogShown;
201+
if (Boolean.TRUE.equals(isDeviceChoiceRequired) && !wasDialogDismissed) {
133202
mDialogType = DialogType.CHOICE_LAUNCH;
134203
mDelegate.updateDialogType(DialogType.CHOICE_LAUNCH);
135-
mDelegate.showDialog();
204+
205+
if (!wasDialogShown) {
206+
mDelegate.showDialog();
207+
}
136208
return;
137209
}
138210

139211
// `isDeviceChoiceRequired` being null indicates that the backend was disconnected, and
140-
// false indicates that blocking the user is not necessary anymore. In both cases we'll want
141-
// to unblock the user, but based on which state the UI is in, we may show some confirmation
142-
// message or not.
143-
144-
if (mIsDialogShown
145-
&& Boolean.FALSE.equals(isDeviceChoiceRequired)
146-
&& mDialogType == DialogType.CHOICE_LAUNCH) {
147-
// This is the normal flow, showing confirmation after the choice has been made.
148-
mDialogType = DialogType.CHOICE_CONFIRM;
149-
mDelegate.updateDialogType(DialogType.CHOICE_CONFIRM);
150-
mSearchEngineChoiceService.notifyDeviceChoiceBlockCleared();
151-
return;
152-
}
212+
// false indicates that blocking the user is not necessary anymore. In both cases we'll
213+
// want to unblock the user, but based on which state the UI is in, we may show some
214+
// confirmation message or not.
153215

154-
if (mIsDialogShown && mDialogType == DialogType.CHOICE_CONFIRM) {
155-
// The backend is sending us some updates while we are showing the confirmation UI. We
156-
// are not blocking anyway and the user can proceed, so don't do anything about it.
157-
return;
216+
if (wasDialogShown && !wasDialogDismissed) {
217+
if (Boolean.FALSE.equals(isDeviceChoiceRequired)
218+
&& (mDialogType == DialogType.LOADING
219+
|| mDialogType == DialogType.CHOICE_LAUNCH)) {
220+
// This is the normal flow, showing confirmation after the choice has been made.
221+
mDialogType = DialogType.CHOICE_CONFIRM;
222+
mDelegate.updateDialogType(DialogType.CHOICE_CONFIRM);
223+
mSearchEngineChoiceService.notifyDeviceChoiceBlockCleared();
224+
return;
225+
}
226+
227+
if (mDialogType == DialogType.CHOICE_CONFIRM) {
228+
// The backend is sending us some updates while we are showing the confirmation UI.
229+
// We are not blocking and the user can proceed, so don't do anything about it.
230+
return;
231+
}
158232
}
159233

160234
// If we get here, this is some sort of error state. Shutdown everything.
161-
// Indicates that the backend was disconnected. This would make the dialog
162-
// non-functional, so let's dismiss it and let the user proceed to Chrome.
163-
// TODO(b/355201070): Add UMA recording.
235+
// Indicates that the backend was disconnected. This would make the dialog non-functional if
236+
// it is still shown, so let's dismiss it and let the user proceed to Chrome.
237+
// TODO(b/355201070): Add UMA recording, remove or update the log below.
164238
Log.w(
165239
TAG,
166240
"Unexpected backend update received. State: "
167-
+ "{mIsDialogShown=%b, mDialogType=%s, isDeviceChoiceRequired=%s}",
168-
mIsDialogShown,
241+
+ "{wasDialogShown=%b, wasDialogDismissed=%b, mDialogType=%s, "
242+
+ "isDeviceChoiceRequired=%s}",
243+
wasDialogShown,
244+
wasDialogDismissed,
169245
mDialogType,
170246
isDeviceChoiceRequired);
171247
mDelegate.dismissDialog();
172248
destroy();
173249
}
250+
251+
private void scheduleDismissOnDeviceChoiceRequiredUpdateTimeout() {
252+
if (mDialogType != DialogType.LOADING) {
253+
return;
254+
}
255+
256+
int dialogTimeoutMillis = SearchEnginesFeatureUtils.clayBlockingDialogTimeoutMillis();
257+
if (dialogTimeoutMillis > 0) {
258+
ThreadUtils.postOnUiThreadDelayed(
259+
() -> {
260+
if (mDialogType != DialogType.LOADING) {
261+
return; // No-op, we got an update.
262+
}
263+
264+
assert mDelegate != null; // Unexpected if the type is still "loading".
265+
266+
Log.w(
267+
TAG,
268+
"Timeout waiting for backend block confirmation. Deadline: %s ms",
269+
dialogTimeoutMillis);
270+
271+
mDelegate.dismissDialog();
272+
destroy();
273+
},
274+
dialogTimeoutMillis);
275+
}
276+
}
174277
}

chrome/browser/search_engines/android/javatests/src/org/chromium/chrome/browser/search_engines/choice_screen/ChoiceScreenRenderTest.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static androidx.test.espresso.assertion.ViewAssertions.matches;
1010
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
1111
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
12+
import static androidx.test.espresso.matcher.ViewMatchers.isNotEnabled;
1213
import static androidx.test.espresso.matcher.ViewMatchers.withId;
1314
import static androidx.test.espresso.matcher.ViewMatchers.withText;
1415

@@ -73,6 +74,7 @@ public class ChoiceScreenRenderTest {
7374
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
7475

7576
private ModalDialogManager mDialogManager;
77+
private FakeSearchEngineCountryDelegate mFakeDelegate;
7678

7779
public ChoiceScreenRenderTest(boolean nightModeEnabled) {
7880
// Sets a fake background color to make the screenshots easier to compare with bare eyes.
@@ -85,12 +87,29 @@ public void setUp() {
8587
FeatureList.setDisableNativeForTesting(true);
8688
mActivityTestRule.launchActivity(null);
8789
mDialogManager = mActivityTestRule.getActivity().getModalDialogManager();
88-
ThreadUtils.runOnUiThreadBlocking(
89-
() ->
90-
SearchEngineChoiceService.setInstanceForTests(
91-
new SearchEngineChoiceService(
92-
new FakeSearchEngineCountryDelegate(
93-
/* enableLogging= */ false))));
90+
mFakeDelegate =
91+
ThreadUtils.runOnUiThreadBlocking(
92+
() -> {
93+
var delegate =
94+
new FakeSearchEngineCountryDelegate(/* enableLogging= */ false);
95+
SearchEngineChoiceService.setInstanceForTests(
96+
new SearchEngineChoiceService(delegate));
97+
return delegate;
98+
});
99+
}
100+
101+
@Test
102+
@LargeTest
103+
@Feature("RenderTest")
104+
public void testLoadingChoiceScreenBlockingDialog() throws Exception {
105+
// Make the delegate not emit a value, putting the UI in the "loading" state.
106+
ThreadUtils.runOnUiThreadBlocking(() -> mFakeDelegate.setIsDeviceChoiceRequired(null));
107+
108+
ThreadUtils.runOnUiThreadBlocking(this::showDialog);
109+
110+
onView(withId(R.id.choice_dialog_button)).inRoot(isDialog()).check(matches(isNotEnabled()));
111+
112+
mRenderTestRule.render(getDialogView(), "loading_choice_screen_blocking_dialog");
94113
}
95114

96115
@Test

0 commit comments

Comments
 (0)