blob: ceccb174c6b64ef2990a7a286b464eb97b3f914f [file] [log] [blame]
// 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.chrome.browser.feed.v2;
import android.app.Activity;
import android.content.Context;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.help.HelpAndFeedback;
import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.offlinepages.RequestCoordinatorBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.IdentityServicesProvider;
import org.chromium.chrome.browser.suggestions.SuggestionsConfig;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.xsurface.FeedActionsHandler;
import org.chromium.chrome.browser.xsurface.HybridListRenderer;
import org.chromium.chrome.browser.xsurface.ProcessScope;
import org.chromium.chrome.browser.xsurface.SurfaceActionsHandler;
import org.chromium.chrome.browser.xsurface.SurfaceDependencyProvider;
import org.chromium.chrome.browser.xsurface.SurfaceScope;
import org.chromium.chrome.browser.xsurface.SurfaceScopeDependencyProvider;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.feed.proto.FeedUiProto.SharedState;
import org.chromium.components.feed.proto.FeedUiProto.Slice;
import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate;
import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate.SliceUpdate;
import org.chromium.components.feed.proto.FeedUiProto.ZeroStateSlice;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.Referrer;
import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.mojom.WindowOpenDisposition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
* Bridge class that lets Android code access native code for feed related functionalities.
*
* Created once for each StreamSurfaceMediator corresponding to each NTP/start surface.
*/
@JNINamespace("feed")
public class FeedStreamSurface implements SurfaceActionsHandler, FeedActionsHandler {
private static final String TAG = "FeedStreamSurface";
private static final int SNACKBAR_DURATION_MS_SHORT = 4000;
private static final int SNACKBAR_DURATION_MS_LONG = 10000;
@VisibleForTesting
static final String FEEDBACK_REPORT_TYPE =
"com.google.chrome.feed.USER_INITIATED_FEEDBACK_REPORT";
@VisibleForTesting
static final String FEEDBACK_CONTEXT = "mobile_browser";
@VisibleForTesting
static final String XSURFACE_CARD_URL = "Card URL";
private final long mNativeFeedStreamSurface;
private final FeedListContentManager mContentManager;
private final SurfaceScope mSurfaceScope;
@VisibleForTesting
RecyclerView mRootView;
private final HybridListRenderer mHybridListRenderer;
private final SnackbarManager mSnackbarManager;
private final Activity mActivity;
private final BottomSheetController mBottomSheetController;
@Nullable
private FeedSliceViewTracker mSliceViewTracker;
private final NativePageNavigationDelegate mPageNavigationDelegate;
private final HelpAndFeedback mHelpAndFeedback;
private int mHeaderCount;
private BottomSheetContent mBottomSheetContent;
// If the bottom sheet was opened in response to an action on a slice, this is the slice ID.
private String mBottomSheetOriginatingSliceId;
private final int mLoadMoreTriggerLookahead;
private boolean mIsLoadingMoreContent;
private static ProcessScope sXSurfaceProcessScope;
public static ProcessScope xSurfaceProcessScope() {
if (sXSurfaceProcessScope == null) {
sXSurfaceProcessScope = AppHooks.get().getExternalSurfaceProcessScope(
new FeedSurfaceDependencyProvider());
}
return sXSurfaceProcessScope;
}
// This must match the FeedSendFeedbackType enum in enums.xml.
public @interface FeedFeedbackType {
int FEEDBACK_TAPPED_ON_CARD = 0;
int FEEDBACK_TAPPED_ON_PAGE = 1;
int NUM_ENTRIES = 2;
}
// We avoid attaching surfaces until after |startup()| is called. This ensures that
// the correct sign-in state is used if attaching the surface triggers a fetch.
private static boolean sStartupCalled;
// Tracks all the surfaces that are waiting to be attached or already attached. When
// |sStartupCalled| is false, |startup()| has not been called and thus all the surfaces
// in this set are waiting to be attached. Otherwise, all the surfaces in this set are
// already attached.
private static HashSet<FeedStreamSurface> sSurfaces;
public static void startup() {
if (sStartupCalled) return;
sStartupCalled = true;
FeedServiceBridge.startup();
xSurfaceProcessScope();
if (sSurfaces != null) {
for (FeedStreamSurface surface : sSurfaces) {
surface.surfaceOpened();
}
}
}
// Only called for cleanup during testing.
@VisibleForTesting
static void shutdownForTesting() {
sStartupCalled = false;
sSurfaces = null;
sXSurfaceProcessScope = null;
}
private static void trackSurface(FeedStreamSurface surface) {
if (sSurfaces == null) {
sSurfaces = new HashSet<FeedStreamSurface>();
}
sSurfaces.add(surface);
}
private static void untrackSurface(FeedStreamSurface surface) {
if (sSurfaces != null) {
sSurfaces.remove(surface);
}
}
/**
* Clear all the data related to all surfaces.
*/
public static void clearAll() {
if (sSurfaces != null) {
for (FeedStreamSurface surface : sSurfaces) {
surface.surfaceClosed();
}
sSurfaces = null;
}
ProcessScope processScope = xSurfaceProcessScope();
if (processScope != null) {
processScope.resetAccount();
}
}
/**
* Provides logging and context for all surfaces.
*
* TODO(rogerm): Find a more global home for this.
*/
private static class FeedSurfaceDependencyProvider implements SurfaceDependencyProvider {
FeedSurfaceDependencyProvider() {}
@Override
public Context getContext() {
return ContextUtils.getApplicationContext();
}
@Override
public String getAccountName() {
CoreAccountInfo primaryAccount =
IdentityServicesProvider.get()
.getIdentityManager(Profile.getLastUsedRegularProfile())
.getPrimaryAccountInfo(ConsentLevel.NOT_REQUIRED);
return primaryAccount == null ? "" : primaryAccount.getEmail();
}
@Override
public int[] getExperimentIds() {
return FeedStreamSurfaceJni.get().getExperimentIds();
}
@Override
public String getClientInstanceId() {
return FeedServiceBridge.getClientInstanceId();
}
}
/**
* Provides activity and darkmode context for a single surface.
*/
private static class FeedSurfaceScopeDependencyProvider
implements SurfaceScopeDependencyProvider {
final Context mActivityContext;
final boolean mDarkMode;
FeedSurfaceScopeDependencyProvider(Context activityContext, boolean darkMode) {
mActivityContext = activityContext;
mDarkMode = darkMode;
}
@Override
public Context getActivityContext() {
return mActivityContext;
}
@Override
public boolean isDarkModeEnabled() {
return mDarkMode;
}
}
/**
* A {@link TabObserver} that observes navigation related events that originate from Feed
* interactions. Calls reportPageLoaded when navigation completes.
*/
private class FeedTabNavigationObserver extends EmptyTabObserver {
private final boolean mInNewTab;
FeedTabNavigationObserver(boolean inNewTab) {
mInNewTab = inNewTab;
}
@Override
public void onPageLoadFinished(Tab tab, String url) {
// TODO(jianli): onPageLoadFinished is called on successful load, and if a user manually
// stops the page load. We should only capture successful page loads.
FeedStreamSurfaceJni.get().reportPageLoaded(
mNativeFeedStreamSurface, FeedStreamSurface.this, url, mInNewTab);
tab.removeObserver(this);
}
@Override
public void onPageLoadFailed(Tab tab, int errorCode) {
tab.removeObserver(this);
}
@Override
public void onCrash(Tab tab) {
tab.removeObserver(this);
}
@Override
public void onDestroyed(Tab tab) {
tab.removeObserver(this);
}
}
/**
* Creates a {@link FeedStreamSurface} for creating native side bridge to access native feed
* client implementation.
*/
public FeedStreamSurface(Activity activity, boolean isBackgroundDark,
SnackbarManager snackbarManager, NativePageNavigationDelegate pageNavigationDelegate,
BottomSheetController bottomSheetController, HelpAndFeedback helpAndFeedback) {
mNativeFeedStreamSurface = FeedStreamSurfaceJni.get().init(FeedStreamSurface.this);
mSnackbarManager = snackbarManager;
mActivity = activity;
mHelpAndFeedback = helpAndFeedback;
mPageNavigationDelegate = pageNavigationDelegate;
mBottomSheetController = bottomSheetController;
mLoadMoreTriggerLookahead = FeedServiceBridge.getLoadMoreTriggerLookahead();
mContentManager = new FeedListContentManager(this, this);
Context context = new ContextThemeWrapper(
activity, (isBackgroundDark ? R.style.Dark : R.style.Light));
ProcessScope processScope = xSurfaceProcessScope();
if (processScope != null) {
mSurfaceScope = processScope.obtainSurfaceScope(
new FeedSurfaceScopeDependencyProvider(context, isBackgroundDark));
;
} else {
mSurfaceScope = null;
}
if (mSurfaceScope != null) {
mHybridListRenderer = mSurfaceScope.provideListRenderer();
} else {
mHybridListRenderer = new NativeViewListRenderer(context);
}
if (mHybridListRenderer != null) {
// XSurface returns a View, but it should be a RecyclerView.
mRootView = (RecyclerView) mHybridListRenderer.bind(mContentManager);
mSliceViewTracker =
new FeedSliceViewTracker(mRootView, mContentManager, (String sliceId) -> {
FeedStreamSurfaceJni.get().reportSliceViewed(
mNativeFeedStreamSurface, FeedStreamSurface.this, sliceId);
});
} else {
mRootView = null;
}
}
/**
* Performs all necessary cleanups.
*/
public void destroy() {
if (mSliceViewTracker != null) {
mSliceViewTracker.destroy();
mSliceViewTracker = null;
}
mHybridListRenderer.unbind();
surfaceClosed();
}
/**
* Puts a list of header views at the beginning.
*/
public void setHeaderViews(List<View> headerViews) {
ArrayList<FeedListContentManager.FeedContent> newContentList =
new ArrayList<FeedListContentManager.FeedContent>();
// First add new header contents. Some of them may appear in the existing list.
for (int i = 0; i < headerViews.size(); ++i) {
View view = headerViews.get(i);
String key = "Header" + view.hashCode();
FeedListContentManager.NativeViewContent headerContent =
new FeedListContentManager.NativeViewContent(key, view);
newContentList.add(headerContent);
}
// Then add all existing feed stream contents.
for (int i = mHeaderCount; i < mContentManager.getItemCount(); ++i) {
newContentList.add(mContentManager.getContent(i));
}
updateContentsInPlace(newContentList);
mHeaderCount = headerViews.size();
}
/**
* @return The android {@link View} that the surface is supposed to show.
*/
public View getView() {
return mRootView;
}
/**
* Attempts to load more content if it can be triggered.
* @return true if loading more content can be triggered.
*/
public boolean maybeLoadMore() {
// Checks if loading more can be triggered.
boolean canLoadMore = false;
LinearLayoutManager layoutManager = (LinearLayoutManager) mRootView.getLayoutManager();
if (layoutManager == null) {
return false;
}
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
if (totalItemCount - lastVisibleItem > mLoadMoreTriggerLookahead) {
return false;
}
// Starts to load more content if not yet.
if (!mIsLoadingMoreContent) {
mIsLoadingMoreContent = true;
FeedStreamSurfaceJni.get().loadMore(mNativeFeedStreamSurface, FeedStreamSurface.this,
(Boolean success) -> { mIsLoadingMoreContent = false; });
}
return true;
}
@VisibleForTesting
FeedListContentManager getFeedListContentManagerForTesting() {
return mContentManager;
}
/**
* Called when the stream update content is available. The content will get passed to UI
*/
@CalledByNative
void onStreamUpdated(byte[] data) {
StreamUpdate streamUpdate;
try {
streamUpdate = StreamUpdate.parseFrom(data);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
Log.wtf(TAG, "Unable to parse StreamUpdate proto data", e);
return;
}
// Update using shared states.
for (SharedState state : streamUpdate.getNewSharedStatesList()) {
mHybridListRenderer.update(state.getXsurfaceSharedState().toByteArray());
}
// Builds the new list containing:
// * existing headers
// * both new and existing contents
ArrayList<FeedListContentManager.FeedContent> newContentList =
new ArrayList<FeedListContentManager.FeedContent>();
for (int i = 0; i < mHeaderCount; ++i) {
newContentList.add(mContentManager.getContent(i));
}
for (SliceUpdate sliceUpdate : streamUpdate.getUpdatedSlicesList()) {
if (sliceUpdate.hasSlice()) {
newContentList.add(createContentFromSlice(sliceUpdate.getSlice()));
} else {
String existingSliceId = sliceUpdate.getSliceId();
int position = mContentManager.findContentPositionByKey(existingSliceId);
if (position != -1) {
newContentList.add(mContentManager.getContent(position));
}
}
}
updateContentsInPlace(newContentList);
}
@CalledByNative
void replaceDataStoreEntry(String key, byte[] data) {
if (mSurfaceScope != null) mSurfaceScope.replaceDataStoreEntry(key, data);
}
@CalledByNative
void removeDataStoreEntry(String key) {
if (mSurfaceScope != null) mSurfaceScope.removeDataStoreEntry(key);
}
private void updateContentsInPlace(
ArrayList<FeedListContentManager.FeedContent> newContentList) {
// 1) Builds the hash set based on keys of new contents.
HashSet<String> newContentKeySet = new HashSet<String>();
for (int i = 0; i < newContentList.size(); ++i) {
newContentKeySet.add(newContentList.get(i).getKey());
}
// 2) Builds the hash map of existing content list for fast look up by key.
HashMap<String, FeedListContentManager.FeedContent> existingContentMap =
new HashMap<String, FeedListContentManager.FeedContent>();
for (int i = 0; i < mContentManager.getItemCount(); ++i) {
FeedListContentManager.FeedContent content = mContentManager.getContent(i);
existingContentMap.put(content.getKey(), content);
}
// 3) Removes those existing contents that do not appear in the new list.
for (int i = mContentManager.getItemCount() - 1; i >= 0; --i) {
String key = mContentManager.getContent(i).getKey();
if (!newContentKeySet.contains(key)) {
mContentManager.removeContents(i, 1);
existingContentMap.remove(key);
}
}
// 4) Iterates through the new list to add the new content or move the existing content
// if needed.
int i = 0;
while (i < newContentList.size()) {
FeedListContentManager.FeedContent content = newContentList.get(i);
// If this is an existing content, moves it to new position.
if (existingContentMap.containsKey(content.getKey())) {
mContentManager.moveContent(
mContentManager.findContentPositionByKey(content.getKey()), i);
++i;
continue;
}
// Otherwise, this is new content. Add it together with all adjacent new contents.
int startIndex = i++;
while (i < newContentList.size()
&& !existingContentMap.containsKey(newContentList.get(i).getKey())) {
++i;
}
mContentManager.addContents(startIndex, newContentList.subList(startIndex, i));
}
}
private FeedListContentManager.FeedContent createContentFromSlice(Slice slice) {
String sliceId = slice.getSliceId();
if (slice.hasXsurfaceSlice()) {
return new FeedListContentManager.ExternalViewContent(
sliceId, slice.getXsurfaceSlice().getXsurfaceFrame().toByteArray());
} else if (slice.hasLoadingSpinnerSlice()) {
return new FeedListContentManager.NativeViewContent(sliceId, R.layout.feed_spinner);
}
assert slice.hasZeroStateSlice();
if (slice.getZeroStateSlice().getType() == ZeroStateSlice.Type.CANT_REFRESH) {
return new FeedListContentManager.NativeViewContent(sliceId, R.layout.no_connection);
}
assert slice.getZeroStateSlice().getType() == ZeroStateSlice.Type.NO_CARDS_AVAILABLE;
return new FeedListContentManager.NativeViewContent(sliceId, R.layout.no_content_v2);
}
/**
* Returns the immediate child of parentView which contains descendentView.
* If descendentView is not in parentView's view heirarchy, this returns null.
* Note that the returned view may be descendentView, or descendentView.getParent(),
* or descendentView.getParent().getParent(), etc...
*/
View findChildViewContainingDescendent(View parentView, View descendentView) {
if (parentView == null || descendentView == null) return null;
// Find the direct child of parentView which owns view.
if (parentView == descendentView.getParent()) {
return descendentView;
} else {
// One of the view's ancestors might be the child.
ViewParent p = descendentView.getParent();
while (true) {
if (p == null) {
return null;
}
if (p.getParent() == parentView) {
if (p instanceof View) return (View) p;
return null;
}
p = p.getParent();
}
}
}
@VisibleForTesting
String getSliceIdFromView(View view) {
View childOfRoot = findChildViewContainingDescendent(mRootView, view);
if (childOfRoot != null) {
// View is a child of the recycler view, find slice using the index.
int position = mRootView.getChildAdapterPosition(childOfRoot);
if (position >= 0 && position < mContentManager.getItemCount()) {
return mContentManager.getContent(position).getKey();
}
} else if (mBottomSheetContent != null
&& findChildViewContainingDescendent(mBottomSheetContent.getContentView(), view)
!= null) {
// View is a child of the bottom sheet, return slice associated with the bottom sheet.
return mBottomSheetOriginatingSliceId;
}
return "";
}
@Override
public void navigateTab(String url, View actionSourceView) {
FeedStreamSurfaceJni.get().reportOpenAction(mNativeFeedStreamSurface,
FeedStreamSurface.this, getSliceIdFromView(actionSourceView));
openUrl(url, WindowOpenDisposition.CURRENT_TAB);
// Attempts to load more content if needed.
maybeLoadMore();
}
@Override
public void navigateNewTab(String url, View actionSourceView) {
FeedStreamSurfaceJni.get().reportOpenInNewTabAction(mNativeFeedStreamSurface,
FeedStreamSurface.this, getSliceIdFromView(actionSourceView));
openUrl(url, WindowOpenDisposition.NEW_FOREGROUND_TAB);
// Attempts to load more content if needed.
maybeLoadMore();
}
@Override
public void navigateIncognitoTab(String url) {
FeedStreamSurfaceJni.get().reportOpenInNewIncognitoTabAction(
mNativeFeedStreamSurface, FeedStreamSurface.this);
openUrl(url, WindowOpenDisposition.OFF_THE_RECORD);
// Attempts to load more content if needed.
maybeLoadMore();
}
@Override
public void downloadLink(String url) {
FeedStreamSurfaceJni.get().reportDownloadAction(
mNativeFeedStreamSurface, FeedStreamSurface.this);
RequestCoordinatorBridge.getForProfile(Profile.getLastUsedRegularProfile())
.savePageLater(
url, OfflinePageBridge.NTP_SUGGESTIONS_NAMESPACE, true /* user requested*/);
}
@Override
public void showBottomSheet(View view, View actionSourceView) {
dismissBottomSheet();
FeedStreamSurfaceJni.get().reportContextMenuOpened(
mNativeFeedStreamSurface, FeedStreamSurface.this);
// Make a sheetContent with the view.
mBottomSheetContent = new CardMenuBottomSheetContent(view);
mBottomSheetOriginatingSliceId = getSliceIdFromView(actionSourceView);
mBottomSheetController.requestShowContent(mBottomSheetContent, true);
}
@Override
public void dismissBottomSheet() {
if (mBottomSheetContent != null) {
mBottomSheetController.hideContent(mBottomSheetContent, true);
}
mBottomSheetContent = null;
mBottomSheetOriginatingSliceId = null;
}
@Override
public void recordActionManageInterests() {
FeedStreamSurfaceJni.get().reportManageInterestsAction(
mNativeFeedStreamSurface, FeedStreamSurface.this);
}
@Override
public void loadMore() {
// TODO(jianli): Remove this from FeedActionsHandler interface.
}
@Override
public void processThereAndBackAgainData(byte[] data) {
FeedStreamSurfaceJni.get().processThereAndBackAgain(
mNativeFeedStreamSurface, FeedStreamSurface.this, data);
}
@Override
public void processViewAction(byte[] data) {
FeedStreamSurfaceJni.get().processViewAction(
mNativeFeedStreamSurface, FeedStreamSurface.this, data);
}
@Override
public void sendFeedback(Map<String, String> productSpecificDataMap) {
FeedStreamSurfaceJni.get().reportSendFeedbackAction(
mNativeFeedStreamSurface, FeedStreamSurface.this);
Profile profile = Profile.getLastUsedRegularProfile();
if (profile == null) {
return;
}
String url = productSpecificDataMap.get(XSURFACE_CARD_URL);
if (url == null) {
return;
}
Map<String, String> feedContext = convertNameFormat(productSpecificDataMap);
// FEEDBACK_CONTEXT: This identifies this feedback as coming from Chrome for Android (as
// opposed to desktop).
// FEEDBACK_REPORT_TYPE: Reports for Chrome mobile must have a contextTag of the form
// com.chrome.feed.USER_INITIATED_FEEDBACK_REPORT, or they will be discarded for not
// matching an allow list rule.
mHelpAndFeedback.showFeedback(
mActivity, profile, url, FEEDBACK_REPORT_TYPE, feedContext, FEEDBACK_CONTEXT);
}
// Since the XSurface client strings are slightly different than the Feed strings, convert the
// name from the XSurface format to the format that can be handled by the feedback system. Any
// new strings that are added on the XSurface side will need a code change here, and adding the
// PSD to the allow list.
private Map<String, String> convertNameFormat(Map<String, String> xSurfaceMap) {
Map<String, String> feedbackNameConversionMap = new HashMap<>();
feedbackNameConversionMap.put("Card URL", "CardUrl");
feedbackNameConversionMap.put("Card Title", "CardTitle");
feedbackNameConversionMap.put("Card Snippet", "CardSnippet");
feedbackNameConversionMap.put("Card category", "CardCategory");
feedbackNameConversionMap.put("Doc Creation Date", "DocCreationDate");
// For each <name, value> entry in the input map, convert the name to the new name, and
// write the new <name, value> pair into the output map.
Map<String, String> feedbackMap = new HashMap<>();
for (Map.Entry<String, String> entry : xSurfaceMap.entrySet()) {
String newName = feedbackNameConversionMap.get(entry.getKey());
if (newName != null) {
feedbackMap.put(newName, entry.getValue());
} else {
Log.v(TAG, "Found an entry with no conversion available.");
// We will put the entry into the map if untranslatable. It will be discarded
// unless it matches an allow list on the server, though. This way we can choose
// to allow it on the server if desired.
feedbackMap.put(entry.getKey(), entry.getValue());
}
}
return feedbackMap;
}
@Override
public int requestDismissal(byte[] data) {
return FeedStreamSurfaceJni.get().executeEphemeralChange(
mNativeFeedStreamSurface, FeedStreamSurface.this, data);
}
@Override
public void commitDismissal(int changeId) {
FeedStreamSurfaceJni.get().commitEphemeralChange(
mNativeFeedStreamSurface, FeedStreamSurface.this, changeId);
// Attempts to load more content if needed.
maybeLoadMore();
}
@Override
public void discardDismissal(int changeId) {
FeedStreamSurfaceJni.get().discardEphemeralChange(
mNativeFeedStreamSurface, FeedStreamSurface.this, changeId);
}
@Override
public void showSnackbar(String text, String actionLabel,
FeedActionsHandler.SnackbarDuration duration,
FeedActionsHandler.SnackbarController controller) {
int durationMs = SNACKBAR_DURATION_MS_SHORT;
if (duration == FeedActionsHandler.SnackbarDuration.LONG) {
durationMs = SNACKBAR_DURATION_MS_LONG;
}
mSnackbarManager.showSnackbar(
Snackbar.make(text,
new SnackbarManager.SnackbarController() {
@Override
public void onAction(Object actionData) {
controller.onAction();
}
@Override
public void onDismissNoAction(Object actionData) {
controller.onDismissNoAction();
}
},
Snackbar.TYPE_ACTION, Snackbar.UMA_FEED_NTP_STREAM)
.setAction(actionLabel, /*actionData=*/null)
.setDuration(durationMs));
}
/**
* Informs that the surface is opened. We can request the initial set of content now. Once
* the content is available, onStreamUpdated will be called.
*/
public void surfaceOpened() {
trackSurface(this);
if (sStartupCalled) {
FeedStreamSurfaceJni.get().surfaceOpened(
mNativeFeedStreamSurface, FeedStreamSurface.this);
}
}
/**
* Informs that the surface is closed.
*/
public void surfaceClosed() {
int feedCount = mContentManager.getItemCount() - mHeaderCount;
if (feedCount > 0) {
mContentManager.removeContents(mHeaderCount, feedCount);
}
untrackSurface(this);
if (sStartupCalled) {
FeedStreamSurfaceJni.get().surfaceClosed(
mNativeFeedStreamSurface, FeedStreamSurface.this);
}
}
private void openUrl(String url, int disposition) {
LoadUrlParams params = new LoadUrlParams(url, PageTransition.AUTO_BOOKMARK);
params.setReferrer(
new Referrer(SuggestionsConfig.getReferrerUrl(ChromeFeatureList.INTEREST_FEED_V2),
ReferrerPolicy.ALWAYS));
Tab tab = mPageNavigationDelegate.openUrl(disposition, params);
boolean inNewTab = (disposition == WindowOpenDisposition.NEW_BACKGROUND_TAB
|| disposition == WindowOpenDisposition.OFF_THE_RECORD);
FeedStreamSurfaceJni.get().reportNavigationStarted(
mNativeFeedStreamSurface, FeedStreamSurface.this);
if (tab != null) {
tab.addObserver(new FeedTabNavigationObserver(inNewTab));
}
}
@NativeMethods
interface Natives {
long init(FeedStreamSurface caller);
int[] getExperimentIds();
void reportSliceViewed(
long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId);
void reportNavigationStarted(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportPageLoaded(long nativeFeedStreamSurface, FeedStreamSurface caller, String url,
boolean inNewTab);
void reportOpenAction(
long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId);
void reportOpenInNewTabAction(
long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId);
void reportOpenInNewIncognitoTabAction(
long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportSendFeedbackAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportDownloadAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportContextMenuOpened(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportManageInterestsAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
// TODO(crbug.com/1111101): These actions aren't visible to the client, so these functions
// are never called.
void reportLearnMoreAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportRemoveAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
void reportNotInterestedInAction(long nativeFeedStreamSurface, FeedStreamSurface caller);
// TODO(jianli): Call this function at the appropriate time.
void reportStreamScrolled(
long nativeFeedStreamSurface, FeedStreamSurface caller, int distanceDp);
// TODO(jianli): Call this function at the appropriate time.
void reportStreamScrollStart(long nativeFeedStreamSurface, FeedStreamSurface caller);
void loadMore(
long nativeFeedStreamSurface, FeedStreamSurface caller, Callback<Boolean> callback);
void processThereAndBackAgain(
long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data);
void processViewAction(long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data);
int executeEphemeralChange(
long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data);
void commitEphemeralChange(
long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId);
void discardEphemeralChange(
long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId);
void surfaceOpened(long nativeFeedStreamSurface, FeedStreamSurface caller);
void surfaceClosed(long nativeFeedStreamSurface, FeedStreamSurface caller);
}
}