Skip to content

Commit

Permalink
Show reader mode as contextual page action
Browse files Browse the repository at this point in the history
This CL adds
- The ability to show reader mode as a CPA using TabDistillabilityProvider.
- Adds a standard interface called ActionProvider to query individual
backends.
- Adds concept of timeout. Introduced a SignalAccumulator to track signals and timeout together.
TODO:
- Add filtering criteria to show reader mode less often such as
restricting reader mode to once per tab, not showing for already
dismissed sites
- Hookup reader mode button click to reader mode manager.

(cherry picked from commit d8786b0)

Bug: 1373444
Change-Id: Ib51df70a55d8dcef7dd82eb16accbd5ac326be00
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3927965
Commit-Queue: Shakti Sahu <shaktisahu@chromium.org>
Reviewed-by: Salvador Guerrero Ramos <salg@google.com>
Cr-Original-Commit-Position: refs/heads/main@{#1059013}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3962153
Reviewed-by: Shakti Sahu <shaktisahu@chromium.org>
Commit-Queue: Salvador Guerrero Ramos <salg@google.com>
Cr-Commit-Position: refs/branch-heads/5359@{#47}
Cr-Branched-From: 27d3765-refs/heads/main@{#1058933}
  • Loading branch information
Shakti Sahu authored and Chromium LUCI CQ committed Oct 17, 2022
1 parent dfce288 commit 3d9f378
Show file tree
Hide file tree
Showing 12 changed files with 578 additions and 114 deletions.
3 changes: 3 additions & 0 deletions chrome/android/chrome_java_sources.gni
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,9 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/searchwidget/SearchType.java",
"java/src/org/chromium/chrome/browser/searchwidget/SearchWidgetProvider.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/ContextualPageActionController.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/PriceTrackingActionProvider.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/ReaderModeActionProvider.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/SignalAccumulator.java",
"java/src/org/chromium/chrome/browser/services/gcm/ChromeGcmListenerServiceImpl.java",
"java/src/org/chromium/chrome/browser/services/gcm/GCMBackgroundServiceImpl.java",
"java/src/org/chromium/chrome/browser/services/gcm/GCMBackgroundTask.java",
Expand Down
3 changes: 3 additions & 0 deletions chrome/android/chrome_junit_test_java_sources.gni
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
chrome_junit_test_java_sources = [
"java/src/org/chromium/chrome/browser/segmentation_platform/ContextualPageActionControllerTest.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/PriceTrackingActionProviderTest.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/ReaderModeActionProviderTest.java",
"java/src/org/chromium/chrome/browser/segmentation_platform/SignalAccumulatorTest.java",
"java/src/org/chromium/chrome/browser/tab/TabFaviconTest.java",
"junit/src/org/chromium/chrome/browser/AppIndexingUtilTest.java",
"junit/src/org/chromium/chrome/browser/BackPressHelperUnitTest.java",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

package org.chromium.chrome.browser.segmentation_platform;

import android.os.Handler;
import android.os.Looper;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.bookmarks.PowerBookmarkUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.CurrentTabObserver;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
Expand All @@ -18,24 +22,41 @@
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures.AdaptiveToolbarButtonVariant;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarStatePredictor;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.segmentation_platform.SegmentSelectionResult;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.List;

/**
* Central class for contextual page actions bridging between UI and backend. Registers itself with
* segmentation platform for on-demand model execution on page load triggers. Provides updated
* button data to the toolbar when asked for it.
*/
public class ContextualPageActionController {
/**
* The interface to be implemented by the individual feature backends to provide signals
* necessary for the controller in an uniform manner.
*/
public interface ActionProvider {
/**
* Called during a page load to fetch the relevant signals from the action provider.
* @param tab The current tab for which the action would be shown.
* @param signalAccumulator An accumulator into which the provider would populate relevant
* signals.
*/
void getAction(Tab tab, SignalAccumulator signalAccumulator);
}

private final ObservableSupplier<Profile> mProfileSupplier;
private final ObservableSupplier<Tab> mTabSupplier;
private final Supplier<ShoppingService> mShoppingServiceSupplier;
private final Supplier<BookmarkModel> mBookmarkModelSupplier;
private ObservableSupplier<Tab> mTabSupplier;
private final AdaptiveToolbarButtonController mAdaptiveToolbarButtonController;
private CurrentTabObserver mCurrentTabObserver;

// The action provider backends.
protected final List<ActionProvider> mActionProviders = new ArrayList<>();

/**
* Constructor.
* @param profileSupplier The supplier for current profile.
Expand All @@ -50,8 +71,6 @@ public ContextualPageActionController(ObservableSupplier<Profile> profileSupplie
Supplier<BookmarkModel> bookmarkModelSupplier) {
mProfileSupplier = profileSupplier;
mTabSupplier = tabSupplier;
mShoppingServiceSupplier = shoppingServiceSupplier;
mBookmarkModelSupplier = bookmarkModelSupplier;
mAdaptiveToolbarButtonController = adaptiveToolbarButtonController;
profileSupplier.addObserver(profile -> {
if (profile.isOffTheRecord()) return;
Expand All @@ -71,9 +90,24 @@ public void didFirstVisuallyNonEmptyPaint(Tab tab) {
if (tab != null) maybeShowContextualPageAction();
}
}, this::activeTabChanged);

initActionProviders(shoppingServiceSupplier, bookmarkModelSupplier);
});
}

@VisibleForTesting
protected void initActionProviders(Supplier<ShoppingService> shoppingServiceSupplier,
Supplier<BookmarkModel> bookmarkModelSupplier) {
mActionProviders.clear();
if (AdaptiveToolbarFeatures.isPriceTrackingPageActionEnabled()) {
mActionProviders.add(new PriceTrackingActionProvider(
shoppingServiceSupplier, bookmarkModelSupplier));
}
if (AdaptiveToolbarFeatures.isReaderModePageActionEnabled()) {
mActionProviders.add(new ReaderModeActionProvider());
}
}

/** Called on destroy. */
public void destroy() {
if (mCurrentTabObserver != null) mCurrentTabObserver.destroy();
Expand All @@ -88,34 +122,28 @@ private void activeTabChanged(Tab tab) {
}

private void maybeShowContextualPageAction() {
Tab tab = mTabSupplier.get();
if (tab == null || tab.isIncognito() || tab.isDestroyed()) {
Tab tab = getValidActiveTab();
if (tab == null) {
// On incognito tabs revert back to static action.
mAdaptiveToolbarButtonController.showDynamicAction(
AdaptiveToolbarButtonVariant.UNKNOWN);
showDynamicAction(AdaptiveToolbarButtonVariant.UNKNOWN);
return;
}
collectSignals(tab);
}

private void collectSignals(Tab tab) {
final BookmarkModel bookmarkModel = mBookmarkModelSupplier.get();
bookmarkModel.finishLoadingBookmarkModel(() -> {
BookmarkId bookmarkId = bookmarkModel.getUserBookmarkIdForTab(tab);
boolean isAlreadyPriceTracked =
PowerBookmarkUtils.isBookmarkPriceTracked(bookmarkModel, bookmarkId);
if (isAlreadyPriceTracked) {
findBestAction(tab, /*canTrackPrice=*/false);
} else {
mShoppingServiceSupplier.get().getProductInfoForUrl(tab.getUrl(), (url, info) -> {
boolean canTrackPrice = info != null;
findBestAction(tab, canTrackPrice);
});
}
});
if (mActionProviders.isEmpty()) return;
final SignalAccumulator signalAccumulator =
new SignalAccumulator(new Handler(Looper.getMainLooper()), tab, mActionProviders);
signalAccumulator.getSignals(() -> findBestAction(signalAccumulator));
}

private void findBestAction(Tab tab, boolean canTrackPrice) {
private void findBestAction(SignalAccumulator signalAccumulator) {
Tab tab = getValidActiveTab();
if (tab == null) return;
// TODO(crbug/1373905): Remove this after segmentation integration for reader mode.
boolean canTrackPrice =
signalAccumulator.hasPriceTracking() || signalAccumulator.hasReaderMode();
ContextualPageActionControllerJni.get().computeContextualPageAction(
mProfileSupplier.get(), tab.getUrl(), canTrackPrice, result -> {
if (tab.isDestroyed()) return;
Expand All @@ -125,13 +153,31 @@ private void findBestAction(Tab tab, boolean canTrackPrice) {
if (!isSameTab) return;

if (!AdaptiveToolbarFeatures.isContextualPageActionUiEnabled()) return;
mAdaptiveToolbarButtonController.showDynamicAction(
AdaptiveToolbarStatePredictor
.getAdaptiveToolbarButtonVariantFromSegmentId(
result.selectedSegment));
int action = AdaptiveToolbarStatePredictor
.getAdaptiveToolbarButtonVariantFromSegmentId(
result.selectedSegment);
// TODO(crbug/1373905): Remove this after segmentation integration for reader
// mode.
if (signalAccumulator.hasReaderMode()) {
action = AdaptiveToolbarButtonVariant.READER_MODE;
}
showDynamicAction(action);
});
}

private void showDynamicAction(@AdaptiveToolbarButtonVariant int action) {
// TODO(crbug/1373891): Add logic to inform reader mode backend.
mAdaptiveToolbarButtonController.showDynamicAction(action);
}

/** @return The active regular tab. Null for incognito. */
private Tab getValidActiveTab() {
if (mProfileSupplier == null || mProfileSupplier.get().isOffTheRecord()) return null;
Tab tab = mTabSupplier.get();
if (tab == null || tab.isIncognito() || tab.isDestroyed()) return null;
return tab;
}

@NativeMethods
interface Natives {
void computeContextualPageAction(Profile profile, GURL url, boolean canTrackPrice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
Expand All @@ -30,6 +29,7 @@
import org.chromium.base.FeatureList;
import org.chromium.base.FeatureList.TestValues;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.Features.EnableFeatures;
Expand All @@ -42,10 +42,6 @@
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarButtonController;
import org.chromium.chrome.browser.toolbar.adaptive.AdaptiveToolbarFeatures.AdaptiveToolbarButtonVariant;
import org.chromium.components.commerce.core.ShoppingService;
import org.chromium.components.commerce.core.ShoppingService.ProductInfo;
import org.chromium.components.commerce.core.ShoppingService.ProductInfoCallback;
import org.chromium.components.power_bookmarks.PowerBookmarkMeta;
import org.chromium.components.power_bookmarks.ShoppingSpecifics;
import org.chromium.components.segmentation_platform.SegmentSelectionResult;
import org.chromium.components.segmentation_platform.proto.SegmentationProto.SegmentId;

Expand Down Expand Up @@ -80,14 +76,6 @@ public class ContextualPageActionControllerTest {
@Rule
public TestRule mProcessor = new Features.JUnitProcessor();

ContextualPageActionController mContextualPageActionController;

@Mock
private ShoppingService mShoppingService;

@Mock
private BookmarkModel mBookmarkModel;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Expand All @@ -98,45 +86,24 @@ public void setUp() {
mJniMocker.mock(ContextualPageActionControllerJni.TEST_HOOKS, mMockControllerJni);
doReturn(mMockConfiguration).when(mMockResources).getConfiguration();
doReturn(true).when(mMockActivityLifecycleDispatcher).isNativeInitializationFinished();

setPriceTrackingBackendResult(false);

// Setup bookmark model expectations.
Mockito.doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
})
.when(mBookmarkModel)
.finishLoadingBookmarkModel(any());
setPageAlreadyPriceTracked(false);
}

private void setPriceTrackingBackendResult(boolean hasProductInfo) {
ProductInfo testProductInfo = new ProductInfo(null, null, 0, 0, null, 0, null);
Mockito.doAnswer(invocation -> {
ProductInfoCallback callback = invocation.getArgument(1);
callback.onResult(
invocation.getArgument(0), hasProductInfo ? testProductInfo : null);
return null;
})
.when(mShoppingService)
.getProductInfoForUrl(any(), any());
}

private void setPageAlreadyPriceTracked(boolean alreadyPriceTracked) {
when(mBookmarkModel.getUserBookmarkIdForTab(any())).thenReturn(null);
PowerBookmarkMeta.Builder builder = PowerBookmarkMeta.newBuilder();
builder.setShoppingSpecifics(
ShoppingSpecifics.newBuilder().setIsPriceTracked(alreadyPriceTracked).build());
when(mBookmarkModel.getPowerBookmarkMeta(any())).thenReturn(builder.build());
}

private ContextualPageActionController createContextualPageActionController() {
ContextualPageActionController contextualPageActionController =
new ContextualPageActionController(mProfileSupplier, mTabSupplier,
mMockAdaptiveToolbarController,
() -> mShoppingService, () -> mBookmarkModel);
mMockAdaptiveToolbarController, null, null) {
@Override
protected void initActionProviders(
Supplier<ShoppingService> shoppingServiceSupplier,
Supplier<BookmarkModel> bookmarkModelSupplier) {
mActionProviders.add((tab, signalAccumulator) -> {
// Supply all signals and notify controller.
signalAccumulator.setHasReaderMode(true);
signalAccumulator.setHasPriceTracking(true);
signalAccumulator.notifySignalAvailable();
});
}
};

mProfileSupplier.set(mMockProfile);

Expand Down Expand Up @@ -188,40 +155,6 @@ public void incognitoTabsRevertToDefaultAction() {
.showDynamicAction(AdaptiveToolbarButtonVariant.UNKNOWN);
}

@Test
public void alreadyPriceTrackedPagesWillBeSkipped() {
mMockConfiguration.screenWidthDp = 450;
setPageAlreadyPriceTracked(true);
setPriceTrackingBackendResult(true);
setMockSegmentationResult(
SegmentId.OPTIMIZATION_TARGET_CONTEXTUAL_PAGE_ACTION_PRICE_TRACKING);

ContextualPageActionController contextualPageActionController =
createContextualPageActionController();

mTabSupplier.set(mMockTab);

verify(mMockControllerJni).computeContextualPageAction(any(), any(), eq(false), any());
}

@Test
public void priceTrackingActionShownSuccessfully() {
mMockConfiguration.screenWidthDp = 450;
setPageAlreadyPriceTracked(true);
setPriceTrackingBackendResult(true);
setMockSegmentationResult(
SegmentId.OPTIMIZATION_TARGET_CONTEXTUAL_PAGE_ACTION_PRICE_TRACKING);

ContextualPageActionController contextualPageActionController =
createContextualPageActionController();

mTabSupplier.set(mMockTab);

verify(mMockControllerJni).computeContextualPageAction(any(), any(), eq(false), any());
verify(mMockAdaptiveToolbarController)
.showDynamicAction(AdaptiveToolbarButtonVariant.PRICE_TRACKING);
}

@Test
public void buttonNotShownWhenUiDisabled() {
mMockConfiguration.screenWidthDp = 450;
Expand All @@ -238,7 +171,6 @@ public void buttonNotShownWhenUiDisabled() {

ContextualPageActionController contextualPageActionController =
createContextualPageActionController();

mTabSupplier.set(mMockTab);

verify(mMockAdaptiveToolbarController, never()).showDynamicAction(anyInt());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.segmentation_platform;

import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.bookmarks.PowerBookmarkUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.commerce.core.ShoppingService;

/** Provides price tracking signal for showing contextual page action for a given tab. */
public class PriceTrackingActionProvider implements ContextualPageActionController.ActionProvider {
private final Supplier<ShoppingService> mShoppingServiceSupplier;
private final Supplier<BookmarkModel> mBookmarkModelSupplier;

/** Constructor. */
public PriceTrackingActionProvider(Supplier<ShoppingService> shoppingServiceSupplier,
Supplier<BookmarkModel> bookmarkModelSupplier) {
mShoppingServiceSupplier = shoppingServiceSupplier;
mBookmarkModelSupplier = bookmarkModelSupplier;
}

@Override
public void getAction(Tab tab, SignalAccumulator signalAccumulator) {
final BookmarkModel bookmarkModel = mBookmarkModelSupplier.get();
bookmarkModel.finishLoadingBookmarkModel(() -> {
BookmarkId bookmarkId = bookmarkModel.getUserBookmarkIdForTab(tab);
boolean isAlreadyPriceTracked =
PowerBookmarkUtils.isBookmarkPriceTracked(bookmarkModel, bookmarkId);
if (isAlreadyPriceTracked) {
signalAccumulator.setHasPriceTracking(false);
signalAccumulator.notifySignalAvailable();
} else {
mShoppingServiceSupplier.get().getProductInfoForUrl(tab.getUrl(), (url, info) -> {
boolean canTrackPrice = info != null;
signalAccumulator.setHasPriceTracking(canTrackPrice);
signalAccumulator.notifySignalAvailable();
});
}
});
}
}
Loading

0 comments on commit 3d9f378

Please sign in to comment.