// 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.compositor.bottombar.contextualsearch; import android.content.Context; import android.os.Handler; import android.text.method.LinkMovementMethod; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import org.chromium.chrome.R; import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel; import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelAnimation; import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelInflater; import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation; import org.chromium.chrome.browser.preferences.PrefServiceBridge; import org.chromium.chrome.browser.preferences.PreferencesLauncher; import org.chromium.chrome.browser.preferences.privacy.ContextualSearchPreferenceFragment; import org.chromium.chrome.browser.util.MathUtils; import org.chromium.ui.base.LocalizationUtils; import org.chromium.ui.resources.dynamics.DynamicResourceLoader; import org.chromium.ui.text.NoUnderlineClickableSpan; import org.chromium.ui.text.SpanApplier; /** * Controls the Search Promo. */ public class ContextualSearchPromoControl extends OverlayPanelInflater implements ChromeAnimation.Animatable<ContextualSearchPromoControl.AnimationType> { /** * Animation types. */ protected enum AnimationType { COLLAPSE } /** * The pixel density. */ private final float mDpToPx; /** * Whether the Promo is visible. */ private boolean mIsVisible; /** * Whether the Promo is mandatory. */ private boolean mIsMandatory; /** * The opacity of the Promo. */ private float mOpacity; /** * The height of the Promo in pixels. */ private float mHeightPx; /** * The height of the Promo content in pixels. */ private float mContentHeightPx; /** * Whether the Promo View is showing. */ private boolean mIsShowingView; /** * The Y position of the Promo View. */ private float mPromoViewY; /** * Whether the Promo was in a state that could be interacted. */ private boolean mWasInteractive; /** * Whether the user's choice has been handled. */ private boolean mHasHandledChoice; /** * The interface used to talk to the Panel. */ private ContextualSearchPromoHost mHost; /** * The delegate that is used to communicate with the Panel. */ public interface ContextualSearchPromoHost { /** * Notifies that the user has opted in. * @param wasMandatory Whether the Promo was mandatory. */ void onPromoOptIn(boolean wasMandatory); /** * Notifies that the user has opted out. */ void onPromoOptOut(); /** * Notifies that the Promo appearance has changed. */ void onUpdatePromoAppearance(); } /** * @param panel The panel. * @param context The Android Context used to inflate the View. * @param container The container View used to inflate the View. * @param resourceLoader The resource loader that will handle the snapshot capturing. */ public ContextualSearchPromoControl(OverlayPanel panel, ContextualSearchPromoHost host, Context context, ViewGroup container, DynamicResourceLoader resourceLoader) { super(panel, R.layout.contextual_search_promo_view, R.id.contextual_search_promo, context, container, resourceLoader); mDpToPx = context.getResources().getDisplayMetrics().density; mHost = host; } // ============================================================================================ // Public API // ============================================================================================ /** * Shows the Promo. This includes inflating the View and setting its initial state. * @param isMandatory Whether the Promo is mandatory. */ public void show(boolean isMandatory) { if (mIsVisible) return; // Invalidates the View in order to generate a snapshot, but do not show the View yet. // The View should only be displayed when in the expanded state. invalidate(); mIsVisible = true; mIsMandatory = isMandatory; mWasInteractive = false; mHeightPx = mContentHeightPx; } /** * Hides the Promo */ public void hide() { if (!mIsVisible) return; hidePromoView(); mIsVisible = false; mIsMandatory = false; mHeightPx = 0.f; mOpacity = 0.f; } /** * Handles change in the Contextual Search preference state. * @param isEnabled Whether the feature was enable. */ public void onContextualSearchPrefChanged(boolean isEnabled) { if (!mIsVisible || !mOverlayPanel.isShowing()) return; if (isEnabled) { boolean wasMandatory = mIsMandatory; // Set mandatory state to false right now because it controls whether the Content // can be displayed. See {@link ContextualSearchPanel#canDisplayContentInPanel}. // Now that the feature is enable, the host will try to show the Contents. // See {@link ContextualSearchPanel#getContextualSearchPromoHost}. mIsMandatory = false; mHost.onPromoOptIn(wasMandatory); } else { mHost.onPromoOptOut(); } collapse(); } /** * @return Whether the Promo is visible. */ public boolean isVisible() { return mIsVisible; } /** * @return Whether the Promo is mandatory. */ public boolean isMandatory() { return mIsMandatory; } /** * @return Whether the Promo reached a state in which it could be interacted. */ public boolean wasInteractive() { return mWasInteractive; } /** * @return The Promo height in pixels. */ public float getHeightPx() { return mHeightPx; } /** * @return The Promo opacity. */ public float getOpacity() { return mOpacity; } // ============================================================================================ // Panel Animation // ============================================================================================ /** * Interpolates the UI from states Closed to Peeked. * * @param percentage The completion percentage. */ public void onUpdateFromCloseToPeek(float percentage) { if (!isVisible()) return; // Promo snapshot should be fully visible here. updateAppearance(1.f); // The View should not be visible in this state. hidePromoView(); } /** * Interpolates the UI from states Peeked to Expanded. * * @param percentage The completion percentage. */ public void onUpdateFromPeekToExpand(float percentage) { if (!isVisible()) return; // Promo snapshot should be fully visible here. updateAppearance(1.f); if (percentage == 1.f) { // We should show the Promo View only when the Panel // has reached the exact expanded height. showPromoView(); } else { // Otherwise the View should not be visible. hidePromoView(); } } /** * Interpolates the UI from states Expanded to Maximized. * * @param percentage The completion percentage. */ public void onUpdateFromExpandToMaximize(float percentage) { if (!isVisible()) return; // Promo snapshot collapses as the Panel reaches the maximized state. updateAppearance(1.f - percentage); // The View should not be visible in this state. hidePromoView(); } // ============================================================================================ // Promo Acceptance Animation // ============================================================================================ @Override public void setProperty(AnimationType type, float value) { if (type == AnimationType.COLLAPSE) { updateAppearance(value); } } @Override public void onPropertyAnimationFinished(AnimationType type) { if (type == AnimationType.COLLAPSE) { hide(); } } /** * Collapses the Promo in an animated fashion. */ public void collapse() { hidePromoView(); mOverlayPanel.addToAnimation(this, AnimationType.COLLAPSE, 1.f, 0.f, OverlayPanelAnimation.BASE_ANIMATION_DURATION_MS, 0); } /** * Updates the appearance of the Promo. * * @param percentage The completion percentage. 0.f means the Promo is fully collapsed and * transparent. 1.f means the Promo is fully expanded and opaque. */ private void updateAppearance(float percentage) { if (mIsVisible) { mHeightPx = Math.round(MathUtils.clamp(percentage * mContentHeightPx, 0.f, mContentHeightPx)); mOpacity = percentage; } else { mHeightPx = 0.f; mOpacity = 0.f; } mHost.onUpdatePromoAppearance(); } // ============================================================================================ // Custom Behaviors // ============================================================================================ @Override public void destroy() { hide(); super.destroy(); } @Override public void invalidate(boolean didViewSizeChange) { super.invalidate(didViewSizeChange); if (didViewSizeChange) { calculatePromoHeight(); } } @Override protected void onFinishInflate() { super.onFinishInflate(); View view = getView(); // "Allow" button. Button allowButton = (Button) view.findViewById(R.id.contextual_search_allow_button); allowButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ContextualSearchPromoControl.this.handlePromoChoice(true); } }); // "No thanks" button. Button noThanksButton = (Button) view.findViewById(R.id.contextual_search_no_thanks_button); noThanksButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ContextualSearchPromoControl.this.handlePromoChoice(false); } }); // Fill in text with link to Settings. TextView promoText = (TextView) view.findViewById(R.id.contextual_search_promo_text); NoUnderlineClickableSpan settingsLink = new NoUnderlineClickableSpan() { @Override public void onClick(View view) { ContextualSearchPromoControl.this.handleClickSettingsLink(); } }; promoText.setText(SpanApplier.applySpans( view.getResources().getString(R.string.contextual_search_short_description), new SpanApplier.SpanInfo("<link>", "</link>", settingsLink))); promoText.setMovementMethod(LinkMovementMethod.getInstance()); calculatePromoHeight(); } @Override protected boolean shouldDetachViewAfterCapturing() { return false; } // ============================================================================================ // Promo Interaction // ============================================================================================ /** * Handles the choice made by the user in the Promo. * @param hasEnabled Whether the user has chosen to enable the feature. */ private void handlePromoChoice(boolean hasEnabled) { if (!mHasHandledChoice) { mHasHandledChoice = true; PrefServiceBridge.getInstance().setContextualSearchState(hasEnabled); } } /** * Handles a click in the settings link located in the Promo. */ private void handleClickSettingsLink() { new Handler().post(new Runnable() { @Override public void run() { PreferencesLauncher.launchSettingsPage(getContext(), ContextualSearchPreferenceFragment.class.getName()); } }); } // ============================================================================================ // Helpers // ============================================================================================ /** * Shows the Promo Android View. By making the Android View visible, we are allowing the * Promo to be interactive. Since snapshots are not interactive (they are just a bitmap), * we need to temporarily show the Android View on top of the snapshot, so the user will * be able to click in the Promo buttons and/or link. */ private void showPromoView() { float y = getYPx(); View view = getView(); if (view == null || !mIsVisible || (mIsShowingView && mPromoViewY == y) || mHeightPx == 0.f) return; float offsetX = mOverlayPanel.getOffsetX() * mDpToPx; if (LocalizationUtils.isLayoutRtl()) { offsetX = -offsetX; } view.setTranslationX(offsetX); view.setTranslationY(y); view.setVisibility(View.VISIBLE); // NOTE(pedrosimonetti): We need to call requestLayout, otherwise // the Promo View will not become visible. view.requestLayout(); mIsShowingView = true; mPromoViewY = y; // The Promo can only be interacted when the View is being displayed. mWasInteractive = true; } /** * Hides the Promo Android View. See {@link #showPromoView()}. */ private void hidePromoView() { View view = getView(); if (view == null || !mIsVisible || !mIsShowingView) { return; } view.setVisibility(View.INVISIBLE); mIsShowingView = false; } /** * @return The current Y position of the Promo. */ private float getYPx() { return Math.round( (mOverlayPanel.getOffsetY() + mOverlayPanel.getBarContainerHeight()) * mDpToPx); } /** * Calculates the content height of the Promo View, and adjusts the height of the Promo while * preserving the proportion of the height with the content height. This should be called * whenever the the size of the Promo View changes. */ private void calculatePromoHeight() { layout(); final float previousContentHeight = mContentHeightPx; mContentHeightPx = getMeasuredHeight(); if (mIsVisible) { // Calculates the ratio between the current height and the previous content height, // and uses it to calculate the new height, while preserving the ratio. final float ratio = mHeightPx / previousContentHeight; mHeightPx = Math.round(mContentHeightPx * ratio); } } }