| // Copyright 2018 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; |
| |
| import android.content.res.Resources; |
| import android.graphics.Rect; |
| import android.support.annotation.Nullable; |
| import android.view.View; |
| import android.widget.ScrollView; |
| |
| import com.google.android.libraries.feed.api.stream.ContentChangedListener; |
| import com.google.android.libraries.feed.api.stream.ScrollListener; |
| import com.google.android.libraries.feed.api.stream.Stream; |
| |
| import org.chromium.base.MemoryPressureListener; |
| import org.chromium.base.memory.MemoryPressureCallback; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.native_page.ContextMenuManager; |
| import org.chromium.chrome.browser.ntp.NewTabPageLayout; |
| import org.chromium.chrome.browser.ntp.SnapScrollHelper; |
| import org.chromium.chrome.browser.ntp.cards.SignInPromo; |
| import org.chromium.chrome.browser.ntp.snippets.SectionHeader; |
| import org.chromium.chrome.browser.preferences.Pref; |
| import org.chromium.chrome.browser.preferences.PrefChangeRegistrar; |
| import org.chromium.chrome.browser.preferences.PrefServiceBridge; |
| import org.chromium.chrome.browser.signin.PersonalizedSigninPromoView; |
| import org.chromium.chrome.browser.signin.SigninPromoUtil; |
| |
| /** |
| * A mediator for the {@link FeedNewTabPage} responsible for interacting with the |
| * native library and handling business logic. |
| */ |
| class FeedNewTabPageMediator |
| implements NewTabPageLayout.ScrollDelegate, ContextMenuManager.TouchEnabledDelegate { |
| private final FeedNewTabPage mCoordinator; |
| private final SnapScrollHelper mSnapScrollHelper; |
| private final PrefChangeRegistrar mPrefChangeRegistrar; |
| |
| private ScrollListener mStreamScrollListener; |
| private ContentChangedListener mStreamContentChangedListener; |
| private SectionHeader mSectionHeader; |
| private MemoryPressureCallback mMemoryPressureCallback; |
| private @Nullable SignInPromo mSignInPromo; |
| |
| private boolean mFeedEnabled; |
| private boolean mTouchEnabled = true; |
| private boolean mStreamContentChanged; |
| private int mThumbnailWidth; |
| private int mThumbnailHeight; |
| private int mThumbnailScrollY; |
| |
| /** |
| * @param feedNewTabPage The {@link FeedNewTabPage} that interacts with this class. |
| * @param snapScrollHelper The {@link SnapScrollHelper} that handles snap scrolling. |
| */ |
| FeedNewTabPageMediator(FeedNewTabPage feedNewTabPage, SnapScrollHelper snapScrollHelper) { |
| mCoordinator = feedNewTabPage; |
| mSnapScrollHelper = snapScrollHelper; |
| |
| mPrefChangeRegistrar = new PrefChangeRegistrar(); |
| mPrefChangeRegistrar.addObserver(Pref.NTP_ARTICLES_SECTION_ENABLED, this::updateContent); |
| initialize(); |
| // Create the content. |
| updateContent(); |
| } |
| |
| /** Clears any dependencies. */ |
| void destroy() { |
| destroyPropertiesForStream(); |
| mPrefChangeRegistrar.destroy(); |
| } |
| |
| private void initialize() { |
| // Listen for layout changes on the NewTabPageView itself to catch changes in scroll |
| // position that are due to layout changes after e.g. device rotation. This contrasts with |
| // regular scrolling, which is observed through an OnScrollListener. |
| mCoordinator.getView().addOnLayoutChangeListener( |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| mSnapScrollHelper.handleScroll(); |
| }); |
| } |
| |
| /** Update the content based on supervised user or enterprise policy. */ |
| private void updateContent() { |
| mFeedEnabled = |
| PrefServiceBridge.getInstance().getBoolean(Pref.NTP_ARTICLES_SECTION_ENABLED); |
| if ((mFeedEnabled && mCoordinator.getStream() != null) |
| || (!mFeedEnabled && mCoordinator.getScrollViewForPolicy() != null)) |
| return; |
| |
| if (mFeedEnabled) { |
| mCoordinator.createStream(); |
| mSnapScrollHelper.setView(mCoordinator.getStream().getView()); |
| initializePropertiesForStream(); |
| } else { |
| destroyPropertiesForStream(); |
| mCoordinator.createScrollViewForPolicy(); |
| mSnapScrollHelper.setView(mCoordinator.getScrollViewForPolicy()); |
| initializePropertiesForPolicy(); |
| } |
| } |
| |
| /** |
| * Initialize properties for UI components in the {@link FeedNewTabPage}. |
| * TODO(huayinz): Introduce a Model for these properties. |
| */ |
| private void initializePropertiesForStream() { |
| Stream stream = mCoordinator.getStream(); |
| mStreamScrollListener = new ScrollListener() { |
| @Override |
| public void onScrollStateChanged(int state) {} |
| |
| @Override |
| public void onScrolled(int dx, int dy) { |
| mSnapScrollHelper.handleScroll(); |
| } |
| }; |
| stream.addScrollListener(mStreamScrollListener); |
| |
| mStreamContentChangedListener = () -> mStreamContentChanged = true; |
| stream.addOnContentChangedListener(mStreamContentChangedListener); |
| |
| boolean suggestionsVisible = |
| PrefServiceBridge.getInstance().getBoolean(Pref.NTP_ARTICLES_LIST_VISIBLE); |
| Resources res = mCoordinator.getSectionHeaderView().getResources(); |
| mSectionHeader = |
| new SectionHeader(res.getString(R.string.ntp_article_suggestions_section_header), |
| suggestionsVisible, this::onSectionHeaderToggled); |
| mPrefChangeRegistrar.addObserver(Pref.NTP_ARTICLES_LIST_VISIBLE, this::updateSectionHeader); |
| mCoordinator.getSectionHeaderView().setHeader(mSectionHeader); |
| stream.setStreamContentVisibility(mSectionHeader.isExpanded()); |
| |
| if (SignInPromo.shouldCreatePromo()) { |
| mSignInPromo = new FeedSignInPromo(); |
| mSignInPromo.setCanShowPersonalizedSuggestions(suggestionsVisible); |
| } |
| |
| mCoordinator.updateHeaderViews(mSignInPromo != null && mSignInPromo.isVisible()); |
| |
| mMemoryPressureCallback = pressure -> mCoordinator.getStream().trim(); |
| MemoryPressureListener.addCallback(mMemoryPressureCallback); |
| } |
| |
| /** Clear any dependencies related to the {@link Stream}. */ |
| private void destroyPropertiesForStream() { |
| Stream stream = mCoordinator.getStream(); |
| if (stream == null) return; |
| |
| stream.removeScrollListener(mStreamScrollListener); |
| stream.removeOnContentChangedListener(mStreamContentChangedListener); |
| MemoryPressureListener.removeCallback(mMemoryPressureCallback); |
| if (mSignInPromo != null) mSignInPromo.destroy(); |
| mPrefChangeRegistrar.removeObserver(Pref.NTP_ARTICLES_LIST_VISIBLE); |
| mStreamScrollListener = null; |
| mStreamContentChangedListener = null; |
| mMemoryPressureCallback = null; |
| mSignInPromo = null; |
| } |
| |
| /** |
| * Initialize properties for the scroll view shown under supervised user or enterprise policy. |
| */ |
| private void initializePropertiesForPolicy() { |
| ScrollView view = mCoordinator.getScrollViewForPolicy(); |
| view.getViewTreeObserver().addOnScrollChangedListener(mSnapScrollHelper::handleScroll); |
| } |
| |
| /** Update whether the section header should be expanded. */ |
| private void updateSectionHeader() { |
| boolean suggestionsVisible = |
| PrefServiceBridge.getInstance().getBoolean(Pref.NTP_ARTICLES_LIST_VISIBLE); |
| if (mSectionHeader.isExpanded() != suggestionsVisible) mSectionHeader.toggleHeader(); |
| if (mSignInPromo != null) { |
| mSignInPromo.setCanShowPersonalizedSuggestions(suggestionsVisible); |
| } |
| } |
| |
| /** |
| * Callback on section header toggled. This will update the visibility of the Feed and the |
| * expand icon on the section header view. |
| */ |
| private void onSectionHeaderToggled() { |
| PrefServiceBridge.getInstance().setBoolean( |
| Pref.NTP_ARTICLES_LIST_VISIBLE, mSectionHeader.isExpanded()); |
| mCoordinator.getStream().setStreamContentVisibility(mSectionHeader.isExpanded()); |
| // TODO(huayinz): Update the section header view through a ModelChangeProcessor. |
| mCoordinator.getSectionHeaderView().updateVisuals(); |
| } |
| |
| /** |
| * Callback on sign-in promo is dismissed. |
| */ |
| void onSignInPromoDismissed() { |
| View view = mCoordinator.getSigninPromoView(); |
| mSignInPromo.dismiss(removedItemTitle |
| -> view.announceForAccessibility(view.getResources().getString( |
| R.string.ntp_accessibility_item_removed, removedItemTitle))); |
| } |
| |
| /** Whether a new thumbnail should be captured. */ |
| boolean shouldCaptureThumbnail() { |
| return mStreamContentChanged || mCoordinator.getView().getWidth() != mThumbnailWidth |
| || mCoordinator.getView().getHeight() != mThumbnailHeight |
| || getVerticalScrollOffset() != mThumbnailScrollY; |
| } |
| |
| /** Reset all the properties for thumbnail capturing after a new thumbnail is captured. */ |
| void onThumbnailCaptured() { |
| mThumbnailWidth = mCoordinator.getView().getWidth(); |
| mThumbnailHeight = mCoordinator.getView().getHeight(); |
| mThumbnailScrollY = getVerticalScrollOffset(); |
| mStreamContentChanged = false; |
| } |
| |
| /** |
| * @return Whether the touch events are enabled on the {@link FeedNewTabPage}. |
| * TODO(huayinz): Move this method to a Model once a Model is introduced. |
| */ |
| boolean getTouchEnabled() { |
| return mTouchEnabled; |
| } |
| |
| // TouchEnabledDelegate interface. |
| |
| @Override |
| public void setTouchEnabled(boolean enabled) { |
| mTouchEnabled = enabled; |
| } |
| |
| // ScrollDelegate interface. |
| |
| @Override |
| public boolean isScrollViewInitialized() { |
| if (mFeedEnabled) { |
| Stream stream = mCoordinator.getStream(); |
| // During startup the view may not be fully initialized, so we check to see if some |
| // basic view properties (height of the RecyclerView) are sane. |
| return stream != null && stream.getView().getHeight() > 0; |
| } else { |
| ScrollView scrollView = mCoordinator.getScrollViewForPolicy(); |
| return scrollView != null && scrollView.getHeight() > 0; |
| } |
| } |
| |
| @Override |
| public int getVerticalScrollOffset() { |
| // This method returns a valid vertical scroll offset only when the first header view in the |
| // Stream is visible. |
| if (!isScrollViewInitialized()) return 0; |
| |
| if (mFeedEnabled) { |
| int firstChildTop = mCoordinator.getStream().getChildTopAt(0); |
| return firstChildTop != Stream.POSITION_NOT_KNOWN ? -firstChildTop : Integer.MIN_VALUE; |
| } else { |
| return mCoordinator.getScrollViewForPolicy().getScrollY(); |
| } |
| } |
| |
| @Override |
| public boolean isChildVisibleAtPosition(int position) { |
| if (mFeedEnabled) { |
| return isScrollViewInitialized() |
| && mCoordinator.getStream().isChildAtPositionVisible(position); |
| } else { |
| ScrollView scrollView = mCoordinator.getScrollViewForPolicy(); |
| Rect rect = new Rect(); |
| scrollView.getHitRect(rect); |
| return scrollView.getChildAt(position).getLocalVisibleRect(rect); |
| } |
| } |
| |
| @Override |
| public void snapScroll() { |
| if (!isScrollViewInitialized()) return; |
| |
| int initialScroll = getVerticalScrollOffset(); |
| int scrollTo = mSnapScrollHelper.calculateSnapPosition(initialScroll); |
| |
| // Calculating the snap position should be idempotent. |
| assert scrollTo == mSnapScrollHelper.calculateSnapPosition(scrollTo); |
| |
| if (mFeedEnabled) { |
| mCoordinator.getStream().smoothScrollBy(0, scrollTo - initialScroll); |
| } else { |
| mCoordinator.getScrollViewForPolicy().smoothScrollBy(0, scrollTo - initialScroll); |
| } |
| } |
| |
| /** |
| * The {@link SignInPromo} for the Feed. |
| * TODO(huayinz): Update content and visibility through a ModelChangeProcessor. |
| */ |
| private class FeedSignInPromo extends SignInPromo { |
| FeedSignInPromo() { |
| updateSignInPromo(); |
| } |
| |
| @Override |
| protected void setVisibilityInternal(boolean visible) { |
| if (isVisible() == visible) return; |
| |
| super.setVisibilityInternal(visible); |
| mCoordinator.updateHeaderViews(visible); |
| } |
| |
| @Override |
| protected void notifyDataChanged() { |
| updateSignInPromo(); |
| } |
| |
| /** Update the content displayed in {@link PersonalizedSigninPromoView}. */ |
| private void updateSignInPromo() { |
| SigninPromoUtil.setupPromoViewFromCache(mSigninPromoController, mProfileDataCache, |
| mCoordinator.getSigninPromoView(), null); |
| } |
| } |
| } |