// Copyright 2015 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.ntp;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.support.annotation.Nullable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback;
import org.chromium.chrome.browser.favicon.FaviconHelper.IconAvailabilityCallback;
import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback;
import org.chromium.chrome.browser.ntp.LogoBridge.Logo;
import org.chromium.chrome.browser.ntp.LogoBridge.LogoObserver;
import org.chromium.chrome.browser.ntp.MostVisitedItem.MostVisitedItemManager;
import org.chromium.chrome.browser.ntp.NewTabPage.OnSearchBoxScrollListener;
import org.chromium.chrome.browser.ntp.cards.CardsVariationParameters;
import org.chromium.chrome.browser.ntp.cards.NewTabPageAdapter;
import org.chromium.chrome.browser.ntp.cards.NewTabPageRecyclerView;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
import org.chromium.chrome.browser.ntp.snippets.SnippetsConfig;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver;
import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import org.chromium.ui.base.DeviceFormFactor;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import jp.tomorrowkey.android.gifplayer.BaseGifImage;
/**
* The native new tab page, represented by some basic data such as title and url, and an Android
* View that displays the page.
*/
public class NewTabPageView extends FrameLayout
implements MostVisitedURLsObserver, OnLayoutChangeListener {
private static final int SHADOW_COLOR = 0x11000000;
private static final long SNAP_SCROLL_DELAY_MS = 30;
private static final String TAG = "Ntp";
/**
* Indicates which UI mode we are using. Should be checked when manipulating some members, as
* they may be unused or {@code null} depending on the mode.
*/
private boolean mUseCardsUi;
// Note: Only one of these will be valid at a time, depending on if we are using the old NTP
// (NewTabPageScrollView) or the new NTP with cards (NewTabPageRecyclerView).
private NewTabPageScrollView mScrollView;
private NewTabPageRecyclerView mRecyclerView;
private NewTabPageLayout mNewTabPageLayout;
private LogoView mSearchProviderLogoView;
private ViewGroup mSearchBoxView;
private ImageView mVoiceSearchButton;
private MostVisitedLayout mMostVisitedLayout;
private View mMostVisitedPlaceholder;
private View mNoSearchLogoSpacer;
/** Adapter for {@link #mRecyclerView}. Will be {@code null} when using the old UI */
private NewTabPageAdapter mNewTabPageAdapter;
private OnSearchBoxScrollListener mSearchBoxScrollListener;
private NewTabPageManager mManager;
private UiConfig mUiConfig;
private MostVisitedDesign mMostVisitedDesign;
private MostVisitedItem[] mMostVisitedItems;
private boolean mFirstShow = true;
private boolean mSearchProviderHasLogo = true;
private boolean mHasReceivedMostVisitedSites;
private boolean mPendingSnapScroll;
/**
* The number of asynchronous tasks that need to complete before the page is done loading.
* This starts at one to track when the view is finished attaching to the window.
*/
private int mPendingLoadTasks = 1;
private boolean mLoadHasCompleted;
private float mUrlFocusChangePercent;
private boolean mDisableUrlFocusChangeAnimations;
/** Flag used to request some layout changes after the next layout pass is completed. */
private boolean mTileCountChanged;
private boolean mSnapshotMostVisitedChanged;
private int mSnapshotWidth;
private int mSnapshotHeight;
private int mSnapshotScrollY;
/**
* Manages the view interaction with the rest of the system.
*/
public interface NewTabPageManager extends MostVisitedItemManager {
/** @return Whether the location bar is shown in the NTP. */
boolean isLocationBarShownInNTP();
/** @return Whether voice search is enabled and the microphone should be shown. */
boolean isVoiceSearchEnabled();
/** @return Whether the omnibox 'Search or type URL' text should be shown. */
boolean isFakeOmniboxTextEnabledTablet();
/** @return Whether context menus should allow the option to open a link in a new window. */
boolean isOpenInNewWindowEnabled();
/** @return Whether context menus should allow the option to open a link in incognito. */
boolean isOpenInIncognitoEnabled();
/** Opens the bookmarks page in the current tab. */
void navigateToBookmarks();
/** Opens the recent tabs page in the current tab. */
void navigateToRecentTabs();
/** Opens the Download Manager UI in the current tab. */
void navigateToDownloadManager();
/**
* Tracks per-page-load metrics for content suggestions.
* @param categories The categories of content suggestions.
* @param suggestionsPerCategory The number of content suggestions in each category.
*/
void trackSnippetsPageImpression(int[] categories, int[] suggestionsPerCategory);
/**
* Tracks impression metrics for a content suggestion.
* @param article The content suggestion that was shown to the user.
*/
void trackSnippetImpression(SnippetArticle article);
/**
* Tracks impression metrics for the long-press menu for a content suggestion.
* @param article The content suggestion for which the long-press menu was opened.
*/
void trackSnippetMenuOpened(SnippetArticle article);
/**
* Tracks impression metrics for a category's action button ("More").
* @param category The category for which the action button was shown.
* @param position The position of the action button within the category.
*/
void trackSnippetCategoryActionImpression(int category, int position);
/**
* Tracks click metrics for a category's action button ("More").
* @param category The category for which the action button was clicked.
* @param position The position of the action button within the category.
*/
void trackSnippetCategoryActionClick(int category, int position);
/**
* Opens a content suggestion and records related metrics.
* @param windowOpenDisposition How to open (current tab, new tab, new window etc).
* @param article The content suggestion to open.
*/
void openSnippet(int windowOpenDisposition, SnippetArticle article);
/**
* Animates the search box up into the omnibox and bring up the keyboard.
* @param beginVoiceSearch Whether to begin a voice search.
* @param pastedText Text to paste in the omnibox after it's been focused. May be null.
*/
void focusSearchBox(boolean beginVoiceSearch, String pastedText);
/**
* Gets the list of most visited sites.
* @param observer The observer to be notified with the list of sites.
* @param numResults The maximum number of sites to retrieve.
*/
void setMostVisitedURLsObserver(MostVisitedURLsObserver observer, int numResults);
/**
* Gets the favicon image for a given URL.
* @param url The URL of the site whose favicon is being requested.
* @param size The desired size of the favicon in pixels.
* @param faviconCallback The callback to be notified when the favicon is available.
*/
void getLocalFaviconImageForURL(
String url, int size, FaviconImageCallback faviconCallback);
/**
* Gets the large icon (e.g. favicon or touch icon) for a given URL.
* @param url The URL of the site whose icon is being requested.
* @param size The desired size of the icon in pixels.
* @param callback The callback to be notified when the icon is available.
*/
void getLargeIconForUrl(String url, int size, LargeIconCallback callback);
/**
* Checks if an icon with the given URL is available. If not,
* downloads it and stores it as a favicon/large icon for the given {@code pageUrl}.
* @param pageUrl The URL of the site whose icon is being requested.
* @param iconUrl The URL of the favicon/large icon.
* @param isLargeIcon Whether the {@code iconUrl} represents a large icon or favicon.
* @param callback The callback to be notified when the favicon has been checked.
*/
void ensureIconIsAvailable(String pageUrl, String iconUrl, boolean isLargeIcon,
boolean isTemporary, IconAvailabilityCallback callback);
/**
* Checks if the pages with the given URLs are available offline.
* @param pageUrls The URLs of the sites whose offline availability is requested.
* @param callback Fired when the results are available.
*/
void getUrlsAvailableOffline(Set<String> pageUrls, Callback<Set<String>> callback);
/**
* Called when the user clicks on the logo.
* @param isAnimatedLogoShowing Whether the animated GIF logo is playing.
*/
void onLogoClicked(boolean isAnimatedLogoShowing);
/**
* Gets the default search provider's logo and calls logoObserver with the result.
* @param logoObserver The callback to notify when the logo is available.
*/
void getSearchProviderLogo(LogoObserver logoObserver);
/**
* Called when the NTP has completely finished loading (all views will be inflated
* and any dependent resources will have been loaded).
* @param mostVisitedItems The MostVisitedItem shown on the NTP. Used to record metrics.
*/
void onLoadingComplete(MostVisitedItem[] mostVisitedItems);
/**
* Passes a {@link Callback} along to the activity to be called whenever a ContextMenu is
* closed.
*/
void addContextMenuCloseCallback(Callback<Menu> callback);
/**
* Passes a {@link Callback} along to the activity to be removed from the list of Callbacks
* called whenever a ContextMenu is closed.
*/
void removeContextMenuCloseCallback(Callback<Menu> callback);
/**
* Makes the {@link Activity} close any open context menu.
*/
void closeContextMenu();
/**
* Handles clicks on the "learn more" link in the footer.
*/
void onLearnMoreClicked();
/**
* Returns the SuggestionsSource or null if it doesn't exist. The SuggestionsSource is
* invalidated (has destroy() called) when the NewTabPage is destroyed so use this method
* instead of keeping your own reference.
*/
@Nullable SuggestionsSource getSuggestionsSource();
/**
* Registers a {@link SignInStateObserver}, will handle the de-registration when the New Tab
* Page goes away.
*/
void registerSignInStateObserver(SignInStateObserver signInStateObserver);
/**
* @return whether the {@link NewTabPage} associated with this manager is the current page
* displayed to the user.
*/
boolean isCurrentPage();
}
/**
* Default constructor required for XML inflation.
*/
public NewTabPageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Initializes the NTP. This must be called immediately after inflation, before this object is
* used in any other way.
*
* @param manager NewTabPageManager used to perform various actions when the user interacts
* with the page.
* @param searchProviderHasLogo Whether the search provider has a logo.
* @param scrollPosition The adapter scroll position to initialize to.
*/
public void initialize(
NewTabPageManager manager, boolean searchProviderHasLogo, int scrollPosition) {
mManager = manager;
mUiConfig = new UiConfig(this);
ViewStub stub = (ViewStub) findViewById(R.id.new_tab_page_layout_stub);
mUseCardsUi = manager.getSuggestionsSource() != null;
if (mUseCardsUi) {
stub.setLayoutResource(R.layout.new_tab_page_recycler_view);
mRecyclerView = (NewTabPageRecyclerView) stub.inflate();
// Don't attach now, the recyclerView itself will determine when to do it.
mNewTabPageLayout =
(NewTabPageLayout) LayoutInflater.from(getContext())
.inflate(R.layout.new_tab_page_layout, mRecyclerView, false);
mNewTabPageLayout.setUseCardsUiEnabled(mUseCardsUi);
mRecyclerView.setAboveTheFoldView(mNewTabPageLayout);
// Tailor the LayoutParams for the snippets UI, as the configuration in the XML is
// made for the ScrollView UI.
ViewGroup.LayoutParams params = mNewTabPageLayout.getLayoutParams();
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public void onAnimationFinished(ViewHolder viewHolder) {
super.onAnimationFinished(viewHolder);
// When removing sections, because the animations are all translations, the
// scroll events don't fire and we can get in the situation where the toolbar
// buttons disappear.
updateSearchBoxOnScroll();
}
});
} else {
stub.setLayoutResource(R.layout.new_tab_page_scroll_view);
mScrollView = (NewTabPageScrollView) stub.inflate();
mScrollView.setBackgroundColor(
NtpStyleUtils.getBackgroundColorResource(getResources(), false));
mScrollView.enableBottomShadow(SHADOW_COLOR);
mNewTabPageLayout = (NewTabPageLayout) findViewById(R.id.ntp_content);
}
mMostVisitedDesign = new MostVisitedDesign(getContext());
mMostVisitedLayout =
(MostVisitedLayout) mNewTabPageLayout.findViewById(R.id.most_visited_layout);
mMostVisitedDesign.initMostVisitedLayout(searchProviderHasLogo);
mSearchProviderLogoView =
(LogoView) mNewTabPageLayout.findViewById(R.id.search_provider_logo);
mSearchBoxView = (ViewGroup) mNewTabPageLayout.findViewById(R.id.search_box);
mNoSearchLogoSpacer = mNewTabPageLayout.findViewById(R.id.no_search_logo_spacer);
initializeSearchBoxTextView();
initializeVoiceSearchButton();
initializeBottomToolbar();
mNewTabPageLayout.addOnLayoutChangeListener(this);
setSearchProviderHasLogo(searchProviderHasLogo);
mPendingLoadTasks++;
mManager.setMostVisitedURLsObserver(
this, mMostVisitedDesign.getNumberOfTiles(searchProviderHasLogo));
// Set up snippets
if (mUseCardsUi) {
mNewTabPageAdapter = NewTabPageAdapter.create(mManager, mNewTabPageLayout, mUiConfig);
mRecyclerView.setAdapter(mNewTabPageAdapter);
mRecyclerView.scrollToPosition(scrollPosition);
if (CardsVariationParameters.isScrollBelowTheFoldEnabled()) {
int searchBoxHeight = NtpStyleUtils.getSearchBoxHeight(getResources());
mRecyclerView.getLinearLayoutManager().scrollToPositionWithOffset(
mNewTabPageAdapter.getFirstHeaderPosition(), searchBoxHeight);
}
// Set up swipe-to-dismiss
ItemTouchHelper helper =
new ItemTouchHelper(mNewTabPageAdapter.getItemTouchCallbacks());
helper.attachToRecyclerView(mRecyclerView);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
private boolean mScrolledOnce = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState != RecyclerView.SCROLL_STATE_DRAGGING) return;
RecordUserAction.record("MobileNTP.Snippets.Scrolled");
if (mScrolledOnce) return;
mScrolledOnce = true;
NewTabPageUma.recordSnippetAction(NewTabPageUma.SNIPPETS_ACTION_SCROLLED);
}
});
initializeSearchBoxRecyclerViewScrollHandling();
} else {
initializeSearchBoxScrollHandling();
}
}
/**
* Sets up the hint text and event handlers for the search box text view.
*/
private void initializeSearchBoxTextView() {
final TextView searchBoxTextView = (TextView) mSearchBoxView
.findViewById(R.id.search_box_text);
String hintText = getResources().getString(R.string.search_or_type_url);
if (!DeviceFormFactor.isTablet(getContext()) || mManager.isFakeOmniboxTextEnabledTablet()) {
searchBoxTextView.setHint(hintText);
} else {
searchBoxTextView.setContentDescription(hintText);
}
searchBoxTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mManager.focusSearchBox(false, null);
}
});
searchBoxTextView.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 0) return;
mManager.focusSearchBox(false, s.toString());
searchBoxTextView.setText("");
}
});
}
private void initializeVoiceSearchButton() {
mVoiceSearchButton = (ImageView) mNewTabPageLayout.findViewById(R.id.voice_search_button);
mVoiceSearchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mManager.focusSearchBox(true, null);
}
});
}
/**
* Sets up event listeners for the bottom toolbar if it is enabled. Removes the bottom toolbar
* if it is disabled.
*/
private void initializeBottomToolbar() {
NewTabPageToolbar toolbar = (NewTabPageToolbar) findViewById(R.id.ntp_toolbar);
if (SnippetsConfig.isEnabled()) {
((ViewGroup) toolbar.getParent()).removeView(toolbar);
MarginLayoutParams params = (MarginLayoutParams) getWrapperView().getLayoutParams();
params.bottomMargin = 0;
} else {
toolbar.getRecentTabsButton().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mManager.navigateToRecentTabs();
}
});
toolbar.getBookmarksButton().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mManager.navigateToBookmarks();
}
});
}
}
private void updateSearchBoxOnScroll() {
if (mDisableUrlFocusChangeAnimations) return;
// When the page changes (tab switching or new page loading), it is possible that events
// (e.g. delayed RecyclerView change notifications) trigger calls to these methods after
// the current page changes. We check it again to make sure we don't attempt to update the
// wrong page.
if (!mManager.isCurrentPage()) return;
if (mSearchBoxScrollListener != null) {
mSearchBoxScrollListener.onNtpScrollChanged(getToolbarTransitionPercentage());
}
}
/**
* Calculates the percentage (between 0 and 1) of the transition from the search box to the
* omnibox at the top of the New Tab Page, which is determined by the amount of scrolling and
* the position of the search box.
*
* @return the transition percentage
*/
private float getToolbarTransitionPercentage() {
// During startup the view may not be fully initialized, so we only calculate the current
// percentage if some basic view properties (height of the containing view, position of the
// search box) are sane.
if (getWrapperView().getHeight() == 0) return 0f;
if (mUseCardsUi && !mRecyclerView.isFirstItemVisible()) {
// getVerticalScroll is valid only for the RecyclerView if the first item is visible.
// If the first item is not visible, we must have scrolled quite far and we know the
// toolbar transition should be 100%. This might be the initial scroll position due to
// the scroll restore feature, so the search box will not have been laid out yet.
return 1f;
}
int searchBoxTop = mSearchBoxView.getTop();
if (searchBoxTop == 0) return 0f;
// For all other calculations, add the search box padding, because it defines where the
// visible "border" of the search box is.
searchBoxTop += mSearchBoxView.getPaddingTop();
if (!mUseCardsUi) {
return MathUtils.clamp(getVerticalScroll() / (float) searchBoxTop, 0f, 1f);
}
final int scrollY = getVerticalScroll();
final float transitionLength =
getResources().getDimension(R.dimen.ntp_search_box_transition_length);
// Tab strip height is zero on phones, nonzero on tablets.
int tabStripHeight = getResources().getDimensionPixelSize(R.dimen.tab_strip_height);
// |scrollY - searchBoxTop + tabStripHeight| gives the distance the search bar is from the
// top of the tab.
return MathUtils.clamp((scrollY - searchBoxTop + transitionLength + tabStripHeight)
/ transitionLength, 0f, 1f);
}
private ViewGroup getWrapperView() {
return mUseCardsUi ? mRecyclerView : mScrollView;
}
/**
* Sets up scrolling when snippets are enabled. It adds scroll listeners and touch listeners to
* the RecyclerView.
*/
private void initializeSearchBoxRecyclerViewScrollHandling() {
final Runnable mSnapScrollRunnable = new Runnable() {
@Override
public void run() {
assert mPendingSnapScroll;
mPendingSnapScroll = false;
mRecyclerView.snapScroll(mSearchBoxView, getVerticalScroll(), getHeight());
}
};
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mPendingSnapScroll) {
mRecyclerView.removeCallbacks(mSnapScrollRunnable);
mRecyclerView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
}
updateSearchBoxOnScroll();
mRecyclerView.updatePeekingCardAndHeader();
}
});
mRecyclerView.setOnTouchListener(new OnTouchListener() {
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(View v, MotionEvent event) {
mRecyclerView.removeCallbacks(mSnapScrollRunnable);
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|| event.getActionMasked() == MotionEvent.ACTION_UP) {
mPendingSnapScroll = true;
mRecyclerView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
} else {
mPendingSnapScroll = false;
}
return false;
}
});
}
/**
* Sets up scrolling when snippets are disabled. It adds scroll and touch listeners to the
* scroll view.
*/
private void initializeSearchBoxScrollHandling() {
final Runnable mSnapScrollRunnable = new Runnable() {
@Override
public void run() {
if (!mPendingSnapScroll) return;
int scrollY = mScrollView.getScrollY();
int dividerTop = mMostVisitedLayout.getTop() - mNewTabPageLayout.getPaddingTop();
if (scrollY > 0 && scrollY < dividerTop) {
mScrollView.smoothScrollTo(0, scrollY < (dividerTop / 2) ? 0 : dividerTop);
}
mPendingSnapScroll = false;
}
};
mScrollView.setOnScrollListener(new NewTabPageScrollView.OnScrollListener() {
@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
if (mPendingSnapScroll) {
mScrollView.removeCallbacks(mSnapScrollRunnable);
mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
}
updateSearchBoxOnScroll();
}
});
mScrollView.setOnTouchListener(new OnTouchListener() {
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(View v, MotionEvent event) {
mScrollView.removeCallbacks(mSnapScrollRunnable);
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|| event.getActionMasked() == MotionEvent.ACTION_UP) {
mPendingSnapScroll = true;
mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
} else {
mPendingSnapScroll = false;
}
return false;
}
});
}
/**
* Decrements the count of pending load tasks and notifies the manager when the page load
* is complete.
*/
private void loadTaskCompleted() {
assert mPendingLoadTasks > 0;
mPendingLoadTasks--;
if (mPendingLoadTasks == 0) {
if (mLoadHasCompleted) {
assert false;
} else {
mLoadHasCompleted = true;
mManager.onLoadingComplete(mMostVisitedItems);
// Load the logo after everything else is finished, since it's lower priority.
loadSearchProviderLogo();
}
}
}
/**
* Loads the search provider logo (e.g. Google doodle), if any.
*/
private void loadSearchProviderLogo() {
mManager.getSearchProviderLogo(new LogoObserver() {
@Override
public void onLogoAvailable(Logo logo, boolean fromCache) {
if (logo == null && fromCache) return;
mSearchProviderLogoView.setMananger(mManager);
mSearchProviderLogoView.updateLogo(logo);
mSnapshotMostVisitedChanged = true;
}
});
}
/**
* Changes the layout depending on whether the selected search provider (e.g. Google, Bing)
* has a logo.
* @param hasLogo Whether the search provider has a logo.
*/
public void setSearchProviderHasLogo(boolean hasLogo) {
if (hasLogo == mSearchProviderHasLogo) return;
mSearchProviderHasLogo = hasLogo;
mMostVisitedDesign.setSearchProviderHasLogo(mMostVisitedLayout, hasLogo);
// Hide or show all the views above the Most Visited items.
int visibility = hasLogo ? View.VISIBLE : View.GONE;
int childCount = mNewTabPageLayout.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mNewTabPageLayout.getChildAt(i);
if (child == mMostVisitedLayout) break;
// Don't change the visibility of a ViewStub as that will automagically inflate it.
if (child instanceof ViewStub) continue;
child.setVisibility(visibility);
}
updateMostVisitedPlaceholderVisibility();
onUrlFocusAnimationChanged();
mSnapshotMostVisitedChanged = true;
}
/**
* Updates whether the NewTabPage should animate on URL focus changes.
* @param disable Whether to disable the animations.
*/
void setUrlFocusAnimationsDisabled(boolean disable) {
if (disable == mDisableUrlFocusChangeAnimations) return;
mDisableUrlFocusChangeAnimations = disable;
if (!disable) onUrlFocusAnimationChanged();
}
/**
* Shows a progressbar indicating the animated logo is being downloaded.
*/
void showLogoLoadingView() {
mSearchProviderLogoView.showLoadingView();
}
/**
* Starts playing the given animated GIF logo.
*/
void playAnimatedLogo(BaseGifImage gifImage) {
mSearchProviderLogoView.playAnimatedLogo(gifImage);
}
/**
* @return Whether URL focus animations are currently disabled.
*/
boolean urlFocusAnimationsDisabled() {
return mDisableUrlFocusChangeAnimations;
}
/**
* Specifies the percentage the URL is focused during an animation. 1.0 specifies that the URL
* bar has focus and has completed the focus animation. 0 is when the URL bar is does not have
* any focus.
*
* @param percent The percentage of the URL bar focus animation.
*/
void setUrlFocusChangeAnimationPercent(float percent) {
mUrlFocusChangePercent = percent;
onUrlFocusAnimationChanged();
}
/**
* @return The percentage that the URL bar is focused during an animation.
*/
@VisibleForTesting
float getUrlFocusChangeAnimationPercent() {
return mUrlFocusChangePercent;
}
private void onUrlFocusAnimationChanged() {
if (mDisableUrlFocusChangeAnimations) return;
float percent = mSearchProviderHasLogo ? mUrlFocusChangePercent : 0;
int basePosition = getVerticalScroll() + mNewTabPageLayout.getPaddingTop();
int target;
if (mUseCardsUi) {
// Cards UI: translate so that the search box is at the top, but only upwards.
target = Math.max(basePosition,
mSearchBoxView.getBottom() - mSearchBoxView.getPaddingBottom());
} else {
// Otherwise: translate so that Most Visited is right below the omnibox.
target = mMostVisitedLayout.getTop();
}
mNewTabPageLayout.setTranslationY(percent * (basePosition - target));
}
/**
* Updates the opacity of the search box when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchBoxAlpha(float alpha) {
mSearchBoxView.setAlpha(alpha);
// Disable the search box contents if it is the process of being animated away.
for (int i = 0; i < mSearchBoxView.getChildCount(); i++) {
mSearchBoxView.getChildAt(i).setEnabled(mSearchBoxView.getAlpha() == 1.0f);
}
}
/**
* Updates the opacity of the search provider logo when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchProviderLogoAlpha(float alpha) {
mSearchProviderLogoView.setAlpha(alpha);
}
/**
* Get the bounds of the search box in relation to the top level NewTabPage view.
*
* @param bounds The current drawing location of the search box.
* @param translation The translation applied to the search box by the parent view hierarchy up
* to the NewTabPage view.
*/
void getSearchBoxBounds(Rect bounds, Point translation) {
int searchBoxX = (int) mSearchBoxView.getX();
int searchBoxY = (int) mSearchBoxView.getY();
bounds.set(searchBoxX + mSearchBoxView.getPaddingLeft(),
searchBoxY + mSearchBoxView.getPaddingTop(),
searchBoxX + mSearchBoxView.getWidth() - mSearchBoxView.getPaddingRight(),
searchBoxY + mSearchBoxView.getHeight() - mSearchBoxView.getPaddingBottom());
translation.set(0, 0);
View view = mSearchBoxView;
while (true) {
view = (View) view.getParent();
if (view == null) {
// The |mSearchBoxView| is not a child of this view. This can happen if the
// RecyclerView detaches the NewTabPageLayout after it has been scrolled out of
// view. Set the translation to the minimum Y value as an approximation.
translation.y = Integer.MIN_VALUE;
break;
}
translation.offset(-view.getScrollX(), -view.getScrollY());
if (view == this) break;
translation.offset((int) view.getX(), (int) view.getY());
}
bounds.offset(translation.x, translation.y);
}
/**
* Sets the listener for search box scroll changes.
* @param listener The listener to be notified on changes.
*/
void setSearchBoxScrollListener(OnSearchBoxScrollListener listener) {
mSearchBoxScrollListener = listener;
if (mSearchBoxScrollListener != null) updateSearchBoxOnScroll();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
assert mManager != null;
if (mFirstShow) {
loadTaskCompleted();
mFirstShow = false;
} else {
// Trigger a scroll update when reattaching the window to signal the toolbar that
// it needs to reset the NTP state.
if (mManager.isLocationBarShownInNTP()) updateSearchBoxOnScroll();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setUrlFocusChangeAnimationPercent(0f);
}
/**
* Update the visibility of the voice search button based on whether the feature is currently
* enabled.
*/
void updateVoiceSearchButtonVisibility() {
mVoiceSearchButton.setVisibility(mManager.isVoiceSearchEnabled() ? VISIBLE : GONE);
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
updateVoiceSearchButtonVisibility();
}
}
/**
* @see org.chromium.chrome.browser.compositor.layouts.content.
* InvalidationAwareThumbnailProvider#shouldCaptureThumbnail()
*/
boolean shouldCaptureThumbnail() {
if (getWidth() == 0 || getHeight() == 0) return false;
return mSnapshotMostVisitedChanged || getWidth() != mSnapshotWidth
|| getHeight() != mSnapshotHeight || getVerticalScroll() != mSnapshotScrollY;
}
/**
* @see org.chromium.chrome.browser.compositor.layouts.content.
* InvalidationAwareThumbnailProvider#captureThumbnail(Canvas)
*/
void captureThumbnail(Canvas canvas) {
mSearchProviderLogoView.endFadeAnimation();
ViewUtils.captureBitmap(this, canvas);
mSnapshotWidth = getWidth();
mSnapshotHeight = getHeight();
mSnapshotScrollY = getVerticalScroll();
mSnapshotMostVisitedChanged = false;
}
// OnLayoutChangeListener overrides
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
int oldHeight = oldBottom - oldTop;
int newHeight = bottom - top;
if (oldHeight == newHeight && !mTileCountChanged) return;
mTileCountChanged = false;
// Re-apply the url focus change amount after a rotation to ensure the views are correctly
// placed with their new layout configurations.
onUrlFocusAnimationChanged();
updateSearchBoxOnScroll();
if (mUseCardsUi) {
mRecyclerView.updatePeekingCardAndHeader();
// The positioning of elements may have been changed (since the elements expand to fill
// the available vertical space), so adjust the scroll.
mRecyclerView.snapScroll(mSearchBoxView, getVerticalScroll(), getHeight());
}
}
// MostVisitedURLsObserver implementation
@Override
public void onMostVisitedURLsAvailable(final String[] titles, final String[] urls,
final String[] whitelistIconPaths, final int[] sources) {
Set<String> urlSet = new HashSet<>(Arrays.asList(urls));
// If no Most Visited items have been built yet, this is the initial load. Build the Most
// Visited items immediately so the layout is stable during initial rendering. They can be
// replaced later if there are offline urls, but that will not affect the layout widths and
// heights. A stable layout enables reliable scroll position initialization.
if (!mHasReceivedMostVisitedSites) {
buildMostVisitedItems(titles, urls, whitelistIconPaths, null, sources);
}
// TODO(https://crbug.com/607573): We should show offline-available content in a nonblocking
// way so that responsiveness of the NTP does not depend on ready availability of offline
// pages.
mManager.getUrlsAvailableOffline(urlSet, new Callback<Set<String>>() {
@Override
public void onResult(Set<String> offlineUrls) {
buildMostVisitedItems(titles, urls, whitelistIconPaths, offlineUrls, sources);
}
});
}
private void buildMostVisitedItems(final String[] titles, final String[] urls,
final String[] whitelistIconPaths, @Nullable final Set<String> offlineUrls,
final int[] sources) {
mMostVisitedLayout.removeAllViews();
MostVisitedItem[] oldItems = mMostVisitedItems;
int oldItemCount = oldItems == null ? 0 : oldItems.length;
mMostVisitedItems = new MostVisitedItem[titles.length];
final boolean isInitialLoad = !mHasReceivedMostVisitedSites;
LayoutInflater inflater = LayoutInflater.from(getContext());
// Add the most visited items to the page.
for (int i = 0; i < titles.length; i++) {
final String url = urls[i];
final String title = titles[i];
final String whitelistIconPath = whitelistIconPaths[i];
final int source = sources[i];
boolean offlineAvailable = offlineUrls != null && offlineUrls.contains(url);
// Look for an existing item to reuse.
MostVisitedItem item = null;
for (int j = 0; j < oldItemCount; j++) {
MostVisitedItem oldItem = oldItems[j];
if (oldItem != null && TextUtils.equals(url, oldItem.getUrl())
&& TextUtils.equals(title, oldItem.getTitle())
&& offlineAvailable == oldItem.isOfflineAvailable()
&& whitelistIconPath.equals(oldItem.getWhitelistIconPath())) {
item = oldItem;
item.setIndex(i);
oldItems[j] = null;
break;
}
}
// If nothing can be reused, create a new item.
if (item == null) {
item = new MostVisitedItem(mManager, title, url, whitelistIconPath,
offlineAvailable, i, source);
View view =
mMostVisitedDesign.createMostVisitedItemView(inflater, item, isInitialLoad);
item.initView(view);
}
mMostVisitedItems[i] = item;
mMostVisitedLayout.addView(item.getView());
}
mHasReceivedMostVisitedSites = true;
updateMostVisitedPlaceholderVisibility();
if (mUrlFocusChangePercent == 1f && oldItemCount != mMostVisitedItems.length) {
// If the number of NTP Tile rows change while the URL bar is focused, the icons'
// position will be wrong. Schedule the translation to be updated.
mTileCountChanged = true;
}
if (isInitialLoad) {
loadTaskCompleted();
// The page contents are initially hidden; otherwise they'll be drawn centered on the
// page before the most visited sites are available and then jump upwards to make space
// once the most visited sites are available.
mNewTabPageLayout.setVisibility(View.VISIBLE);
}
mSnapshotMostVisitedChanged = true;
}
@Override
public void onPopularURLsAvailable(
String[] urls, String[] faviconUrls, String[] largeIconUrls) {
for (int i = 0; i < urls.length; i++) {
final String url = urls[i];
boolean useLargeIcon = !largeIconUrls[i].isEmpty();
// Only fetch one of favicon or large icon based on what is required on the NTP.
// The other will be fetched on visiting the site.
String iconUrl = useLargeIcon ? largeIconUrls[i] : faviconUrls[i];
if (iconUrl.isEmpty()) continue;
IconAvailabilityCallback callback = new IconAvailabilityCallback() {
@Override
public void onIconAvailabilityChecked(boolean newlyAvailable) {
if (newlyAvailable) {
mMostVisitedDesign.onIconUpdated(url);
}
}
};
mManager.ensureIconIsAvailable(
url, iconUrl, useLargeIcon, /*isTemporary=*/false, callback);
}
}
/**
* Shows the most visited placeholder ("Nothing to see here") if there are no most visited
* items and there is no search provider logo.
*/
private void updateMostVisitedPlaceholderVisibility() {
boolean showPlaceholder = mHasReceivedMostVisitedSites
&& mMostVisitedLayout.getChildCount() == 0
&& !mSearchProviderHasLogo;
mNoSearchLogoSpacer.setVisibility(
(mSearchProviderHasLogo || showPlaceholder) ? View.GONE : View.INVISIBLE);
if (showPlaceholder) {
if (mMostVisitedPlaceholder == null) {
ViewStub mostVisitedPlaceholderStub = (ViewStub) mNewTabPageLayout
.findViewById(R.id.most_visited_placeholder_stub);
mMostVisitedPlaceholder = mostVisitedPlaceholderStub.inflate();
}
mMostVisitedLayout.setVisibility(GONE);
mMostVisitedPlaceholder.setVisibility(VISIBLE);
} else if (mMostVisitedPlaceholder != null) {
mMostVisitedLayout.setVisibility(VISIBLE);
mMostVisitedPlaceholder.setVisibility(GONE);
}
}
/**
* The design for most visited tiles: each tile shows a large icon and the site's title.
*/
private class MostVisitedDesign {
private static final int NUM_TILES = 8;
private static final int NUM_TILES_NO_LOGO = 12;
private static final int MAX_ROWS = 2;
private static final int MAX_ROWS_NO_LOGO = 3;
private static final int ICON_CORNER_RADIUS_DP = 4;
private static final int ICON_TEXT_SIZE_DP = 20;
private static final int ICON_MIN_SIZE_PX = 48;
private int mMinIconSize;
private int mDesiredIconSize;
private RoundedIconGenerator mIconGenerator;
MostVisitedDesign(Context context) {
Resources res = context.getResources();
mDesiredIconSize = res.getDimensionPixelSize(R.dimen.most_visited_icon_size);
// On ldpi devices, mDesiredIconSize could be even smaller than ICON_MIN_SIZE_PX.
mMinIconSize = Math.min(mDesiredIconSize, ICON_MIN_SIZE_PX);
int desiredIconSizeDp = Math.round(
mDesiredIconSize / res.getDisplayMetrics().density);
int iconColor = ApiCompatibilityUtils.getColor(
getResources(), R.color.default_favicon_background_color);
mIconGenerator = new RoundedIconGenerator(context, desiredIconSizeDp, desiredIconSizeDp,
ICON_CORNER_RADIUS_DP, iconColor, ICON_TEXT_SIZE_DP);
}
public int getNumberOfTiles(boolean searchProviderHasLogo) {
return searchProviderHasLogo ? NUM_TILES : NUM_TILES_NO_LOGO;
}
public void initMostVisitedLayout(boolean searchProviderHasLogo) {
mMostVisitedLayout.setMaxRows(searchProviderHasLogo ? MAX_ROWS : MAX_ROWS_NO_LOGO);
}
public void setSearchProviderHasLogo(View mostVisitedLayout, boolean hasLogo) {
int paddingTop = getResources().getDimensionPixelSize(hasLogo
? R.dimen.most_visited_layout_padding_top
: R.dimen.most_visited_layout_no_logo_padding_top);
mostVisitedLayout.setPadding(0, paddingTop, 0, mMostVisitedLayout.getPaddingBottom());
}
class LargeIconCallbackImpl implements LargeIconCallback {
private MostVisitedItem mItem;
private MostVisitedItemView mItemView;
private boolean mIsInitialLoad;
public LargeIconCallbackImpl(
MostVisitedItem item, MostVisitedItemView itemView, boolean isInitialLoad) {
mItem = item;
mItemView = itemView;
mIsInitialLoad = isInitialLoad;
}
@Override
public void onLargeIconAvailable(
Bitmap icon, int fallbackColor, boolean isFallbackColorDefault) {
if (icon == null) {
mIconGenerator.setBackgroundColor(fallbackColor);
icon = mIconGenerator.generateIconForUrl(mItem.getUrl());
mItemView.setIcon(new BitmapDrawable(getResources(), icon));
mItem.setTileType(isFallbackColorDefault ? MostVisitedTileType.ICON_DEFAULT
: MostVisitedTileType.ICON_COLOR);
} else {
RoundedBitmapDrawable roundedIcon = RoundedBitmapDrawableFactory.create(
getResources(), icon);
int cornerRadius = Math.round(ICON_CORNER_RADIUS_DP
* getResources().getDisplayMetrics().density * icon.getWidth()
/ mDesiredIconSize);
roundedIcon.setCornerRadius(cornerRadius);
roundedIcon.setAntiAlias(true);
roundedIcon.setFilterBitmap(true);
mItemView.setIcon(roundedIcon);
mItem.setTileType(MostVisitedTileType.ICON_REAL);
}
mSnapshotMostVisitedChanged = true;
if (mIsInitialLoad) loadTaskCompleted();
}
}
public View createMostVisitedItemView(
LayoutInflater inflater, MostVisitedItem item, boolean isInitialLoad) {
final MostVisitedItemView view = (MostVisitedItemView) inflater.inflate(
R.layout.most_visited_item, mMostVisitedLayout, false);
view.setTitle(TitleUtil.getTitleForDisplay(item.getTitle(), item.getUrl()));
view.setOfflineAvailable(item.isOfflineAvailable());
LargeIconCallback iconCallback = new LargeIconCallbackImpl(item, view, isInitialLoad);
if (isInitialLoad) mPendingLoadTasks++;
if (!loadWhitelistIcon(item, iconCallback)) {
mManager.getLargeIconForUrl(item.getUrl(), mMinIconSize, iconCallback);
}
return view;
}
private boolean loadWhitelistIcon(MostVisitedItem item, LargeIconCallback iconCallback) {
if (item.getWhitelistIconPath().isEmpty()) return false;
Bitmap bitmap = BitmapFactory.decodeFile(item.getWhitelistIconPath());
if (bitmap == null) {
Log.d(TAG, "Image decoding failed: %s", item.getWhitelistIconPath());
return false;
}
iconCallback.onLargeIconAvailable(bitmap, Color.BLACK, false);
return true;
}
public void onIconUpdated(final String url) {
if (mMostVisitedItems == null) return;
// Find a matching most visited item.
for (MostVisitedItem item : mMostVisitedItems) {
if (item.getUrl().equals(url)) {
LargeIconCallback iconCallback = new LargeIconCallbackImpl(
item, (MostVisitedItemView) item.getView(), false);
mManager.getLargeIconForUrl(url, mMinIconSize, iconCallback);
break;
}
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mNewTabPageLayout != null) {
mNewTabPageLayout.setParentViewportHeight(MeasureSpec.getSize(heightMeasureSpec));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mUseCardsUi) mRecyclerView.updatePeekingCardAndHeader();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// When the viewport configuration changes, we want to update the display style so that the
// observers are aware of the new available space. Another moment to do this update could
// be through a OnLayoutChangeListener, but then we get notified of the change after the
// layout pass, which means that the new style will only be visible after layout happens
// again. We prefer updating here to avoid having to require that additional layout pass.
mUiConfig.updateDisplayStyle();
// Close the Context Menu as it may have moved (https://crbug.com/642688).
mManager.closeContextMenu();
}
private int getVerticalScroll() {
if (mUseCardsUi) {
return mRecyclerView.computeVerticalScrollOffset();
} else {
return mScrollView.getScrollY();
}
}
/**
* @return The adapter position the user has scrolled to.
*/
public int getScrollPosition() {
if (mUseCardsUi) return mRecyclerView.getScrollPosition();
return RecyclerView.NO_POSITION;
}
}