Commit 3407f0e0 authored by Mehran Mahmoudi's avatar Mehran Mahmoudi Committed by Commit Bot

[Paint Preview] Add Snackbar to TabbedPaintPreviewPlayer

This shows a snackbar when the user interacts with the paint preview
player on startup.

It also implements the 'frustration detector'. It triggers when the user
taps in areas that are not links, or longpresses. This will lead to
showing the snackbar again.

Bug: 1106430
Change-Id: I3aa94f055a730630058bd204d0810e81668b7a36
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2302938
Commit-Queue: Mehran Mahmoudi <mahmoudi@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Reviewed-by: default avatarCalder Kitagawa <ckitagawa@chromium.org>
Reviewed-by: default avatarFred Mello <fredmello@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790991}
parent 11875d18
......@@ -33,6 +33,7 @@ android_library("java") {
"//chrome/browser/flags:java",
"//chrome/browser/tab:java",
"//chrome/browser/tabmodel:java",
"//chrome/browser/ui/messages/android:java",
"//components/browser_ui/styles/android:java",
"//components/paint_preview/browser/android:java",
"//components/paint_preview/player/android:java",
......
......@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.paint_preview;
import android.content.res.Resources;
import android.view.View;
import androidx.annotation.Nullable;
......@@ -15,6 +16,9 @@ import org.chromium.chrome.browser.paint_preview.services.PaintPreviewTabService
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabViewProvider;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManagerProvider;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.paintpreview.player.PlayerManager;
import org.chromium.content_public.browser.LoadUrlParams;
......@@ -28,6 +32,8 @@ public class TabbedPaintPreviewPlayer implements TabViewProvider, UserData {
public static final Class<TabbedPaintPreviewPlayer> USER_DATA_KEY =
TabbedPaintPreviewPlayer.class;
private static final int SNACKBAR_DURATION_MS = 5 * 1000;
private Tab mTab;
private PaintPreviewTabService mPaintPreviewTabService;
private PlayerManager mPlayerManager;
......@@ -35,6 +41,7 @@ public class TabbedPaintPreviewPlayer implements TabViewProvider, UserData {
private Boolean mInitializing;
private boolean mHasUserInteraction;
private EmptyTabObserver mTabObserver;
private long mLastShownSnackBarTime;
public static TabbedPaintPreviewPlayer get(Tab tab) {
if (tab.getUserDataHost().getUserData(USER_DATA_KEY) == null) {
......@@ -49,11 +56,19 @@ public class TabbedPaintPreviewPlayer implements TabViewProvider, UserData {
mTabObserver = new EmptyTabObserver() {
@Override
public void didFirstVisuallyNonEmptyPaint(Tab tab) {
if (mTab.getTabViewManager().isShowing(TabbedPaintPreviewPlayer.this)
&& !mHasUserInteraction) {
if (!mTab.getTabViewManager().isShowing(TabbedPaintPreviewPlayer.this)) {
return;
}
if (!mHasUserInteraction) {
removePaintPreview();
return;
}
showSnackbar();
}
};
mTab.addObserver(mTabObserver);
}
......@@ -84,6 +99,7 @@ public class TabbedPaintPreviewPlayer implements TabViewProvider, UserData {
() -> mHasUserInteraction = true,
ChromeColors.getPrimaryBackgroundColor(mTab.getContext().getResources(), false),
this::removePaintPreview, /*ignoreInitialScrollOffset=*/false);
mPlayerManager.setUserFrustrationCallback(this::showSnackbar);
mOnDismissed = onDismissed;
mTab.getTabViewManager().addTabViewProvider(this);
return true;
......@@ -104,6 +120,32 @@ public class TabbedPaintPreviewPlayer implements TabViewProvider, UserData {
RecordUserAction.record("PaintPreview.TabbedPlayer.Removed");
}
private void showSnackbar() {
if (mTab == null || mTab.getWindowAndroid() == null) return;
// If the Snackbar is already being displayed, return.
if (System.currentTimeMillis() - mLastShownSnackBarTime < SNACKBAR_DURATION_MS) return;
Resources resources = mTab.getContext().getResources();
Snackbar snackbar = Snackbar.make(
resources.getString(R.string.paint_preview_startup_upgrade_snackbar_message),
new SnackbarManager.SnackbarController() {
@Override
public void onAction(Object actionData) {
removePaintPreview();
}
@Override
public void onDismissNoAction(Object actionData) {}
},
Snackbar.TYPE_NOTIFICATION, Snackbar.UMA_PAINT_PREVIEW_UPGRADE_NOTIFICATION);
snackbar.setAction(
resources.getString(R.string.paint_preview_startup_upgrade_snackbar_action), null);
snackbar.setDuration(SNACKBAR_DURATION_MS);
SnackbarManagerProvider.from(mTab.getWindowAndroid()).showSnackbar(snackbar);
mLastShownSnackBarTime = System.currentTimeMillis();
}
public boolean isShowingAndNeedsBadge() {
return mTab.getTabViewManager().isShowing(this);
}
......
......@@ -3854,6 +3854,14 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
Paint Preview playback failed.
</message>
<!-- Paint Preview Startup Experiment -->
<message name="IDS_PAINT_PREVIEW_STARTUP_UPGRADE_SNACKBAR_MESSAGE" desc="Message displayed on a snackbar when a paint preview is shown on startup, telling the user that this is a preview of the page" translateable="false">
This is a preview
</message>
<message name="IDS_PAINT_PREVIEW_STARTUP_UPGRADE_SNACKBAR_ACTION" desc="Text displayed on the action button of snackbar, promting user to switch to the live page and exist paint preivew." translateable="false">
View live page
</message>
<!-- Default Browser Promo Strings-->
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_TITLE" desc="Title of the default browser promo dialog">
Set <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph> as your default?
......
......@@ -88,6 +88,7 @@ public class Snackbar {
public static final int UMA_TWA_PRIVACY_DISCLOSURE_V2 = 33;
public static final int UMA_HOMEPAGE_PROMO_CHANGED_UNDO = 34;
public static final int UMA_CONDITIONAL_TAB_STRIP_DISMISS_UNDO = 35;
public static final int UMA_PAINT_PREVIEW_UPGRADE_NOTIFICATION = 36;
private SnackbarController mController;
private CharSequence mText;
......
......@@ -56,6 +56,7 @@ android_library("java") {
"java/src/org/chromium/components/paintpreview/player/PlayerManager.java",
"java/src/org/chromium/components/paintpreview/player/PlayerSwipeRefreshHandler.java",
"java/src/org/chromium/components/paintpreview/player/PlayerUserActionRecorder.java",
"java/src/org/chromium/components/paintpreview/player/PlayerUserFrustrationDetector.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainter.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapState.java",
"java/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapStateController.java",
......@@ -157,6 +158,7 @@ source_set("test_support") {
junit_binary("paint_preview_junit_tests") {
sources = [
"junit/src/org/chromium/components/paintpreview/player/PlayerManagerTest.java",
"junit/src/org/chromium/components/paintpreview/player/PlayerUserFrustrationDetectorTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PaintPreviewCustomFlingingShadowScroller.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameBitmapPainterTest.java",
"junit/src/org/chromium/components/paintpreview/player/frame/PlayerFrameMediatorTest.java",
......
......@@ -13,6 +13,7 @@ import org.chromium.url.GURL;
public class PlayerGestureListener {
private Runnable mUserInteractionCallback;
private LinkClickHandler mLinkClickHandler;
private PlayerUserFrustrationDetector mUserFrustrationDetector;
public PlayerGestureListener(
LinkClickHandler linkClickHandler, Runnable userInteractionCallback) {
......@@ -32,10 +33,12 @@ public class PlayerGestureListener {
return;
}
if (mUserFrustrationDetector != null) mUserFrustrationDetector.recordUnconsumedTap();
PlayerUserActionRecorder.recordUnconsumedTap();
}
public void onLongPress() {
if (mUserFrustrationDetector != null) mUserFrustrationDetector.recordUnconsumedLongPress();
PlayerUserActionRecorder.recordLongPress();
}
......@@ -53,4 +56,8 @@ public class PlayerGestureListener {
if (mUserInteractionCallback != null) mUserInteractionCallback.run();
if (didFinish) PlayerUserActionRecorder.recordZoom();
}
public void setUserFrustrationDetector(PlayerUserFrustrationDetector userFrustrationDetector) {
mUserFrustrationDetector = userFrustrationDetector;
}
}
......@@ -79,6 +79,12 @@ public class PlayerManager {
mIgnoreInitialScrollOffset = ignoreInitialScrollOffset;
}
public void setUserFrustrationCallback(Runnable userFrustrationCallback) {
PlayerUserFrustrationDetector userFrustrationDetector =
new PlayerUserFrustrationDetector(userFrustrationCallback);
mPlayerGestureListener.setUserFrustrationDetector(userFrustrationDetector);
}
/**
* Called by {@link PlayerCompositorDelegateImpl} when the compositor is initialized. This
* method initializes a sub-component for each frame and adds the view for the root frame to
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/** Uses touch gesture-based heuristics to detect use frustration. */
public class PlayerUserFrustrationDetector {
private Runnable mFrustrationDetectionCallback;
private List<Long> mTapsTimeMs = new ArrayList<>();
static final int CONSECUTIVE_SINGLE_TAP_WINDOW_MS = 2 * 1000;
static final int CONSECUTIVE_SINGLE_TAP_COUNT = 3;
public PlayerUserFrustrationDetector(Runnable frustrationDetectionCallback) {
mFrustrationDetectionCallback = frustrationDetectionCallback;
}
void recordUnconsumedTap() {
recordUnconsumedTap(System.currentTimeMillis());
}
@VisibleForTesting
void recordUnconsumedTap(long timeMs) {
mTapsTimeMs.add(timeMs);
for (int i = mTapsTimeMs.size() - 1; i > 0; --i) {
if (mTapsTimeMs.get(i) - mTapsTimeMs.get(i - 1) > CONSECUTIVE_SINGLE_TAP_WINDOW_MS) {
mTapsTimeMs.subList(0, i).clear();
break;
}
}
if (mTapsTimeMs.size() == CONSECUTIVE_SINGLE_TAP_COUNT) {
mFrustrationDetectionCallback.run();
mTapsTimeMs.clear();
}
}
void recordUnconsumedLongPress() {
mFrustrationDetectionCallback.run();
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.paintpreview.player;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
/**
* Tests for the {@link PlayerUserFrustrationDetector} class
*/
@RunWith(BaseRobolectricTestRunner.class)
public class PlayerUserFrustrationDetectorTest {
@Test
public void testConsecutiveTapDetection() {
CallbackHelper detectionCallback = new CallbackHelper();
PlayerUserFrustrationDetector detector =
new PlayerUserFrustrationDetector(detectionCallback::notifyCalled);
final int tapsWindow = PlayerUserFrustrationDetector.CONSECUTIVE_SINGLE_TAP_WINDOW_MS;
final int tapsCount = PlayerUserFrustrationDetector.CONSECUTIVE_SINGLE_TAP_COUNT;
long startTime = System.currentTimeMillis();
// Record |tapsCount| consecutive taps in shorter than |tapsWindow| intervals. This should
// trigger the detection.
Assert.assertEquals("Frustration callback shouldn't have been called", 0,
detectionCallback.getCallCount());
for (int i = 0; i < tapsCount; i++) {
detector.recordUnconsumedTap(startTime + i * tapsWindow);
}
Assert.assertEquals("Frustration callback should have been called once", 1,
detectionCallback.getCallCount());
// A new tap, even if it's within the window, should not trigger the callback.
detector.recordUnconsumedTap(startTime + tapsCount * tapsWindow);
Assert.assertEquals("Frustration callback shouldn't have been called", 1,
detectionCallback.getCallCount());
// Perform |tapsCount - 1| series of consecutive taps. Each time, delay one of the taps
// so it's out of the |tapsWindow|.
// None of these should result in a frustration trigger.
for (int i = 1; i < tapsCount; i++) {
// Increment start time so it won't be within a valid window with the previous tap.
startTime += (tapsCount + 2) * tapsWindow;
for (int j = 0; j < tapsCount; j++) {
int delay = i == j ? 10 : 0;
detector.recordUnconsumedTap(startTime + j * tapsWindow + delay);
}
Assert.assertEquals("Frustration callback shouldn't have been called", 1,
detectionCallback.getCallCount());
}
// Perform a valid tap sequence.
startTime += (tapsCount + 2) * tapsWindow;
for (int i = 0; i < tapsCount; i++) {
detector.recordUnconsumedTap(startTime + i * tapsWindow);
}
Assert.assertEquals("Frustration callback should have been called once", 2,
detectionCallback.getCallCount());
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment