michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.EnumSet; michael@0: import java.util.List; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.animation.PropertyAnimator; michael@0: import org.mozilla.gecko.animation.ViewHelper; michael@0: import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener; michael@0: import org.mozilla.gecko.home.HomeConfig.PanelConfig; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.content.Context; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.os.Build; michael@0: import android.os.Bundle; michael@0: import android.support.v4.app.FragmentManager; michael@0: import android.support.v4.app.LoaderManager; michael@0: import android.support.v4.app.LoaderManager.LoaderCallbacks; michael@0: import android.support.v4.content.Loader; michael@0: import android.support.v4.view.ViewPager; michael@0: import android.util.AttributeSet; michael@0: import android.view.MotionEvent; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: michael@0: public class HomePager extends ViewPager { michael@0: private static final int LOADER_ID_CONFIG = 0; michael@0: michael@0: private final Context mContext; michael@0: private volatile boolean mVisible; michael@0: private Decor mDecor; michael@0: private View mTabStrip; michael@0: private HomeBanner mHomeBanner; michael@0: private int mDefaultPageIndex = -1; michael@0: michael@0: private final OnAddPanelListener mAddPanelListener; michael@0: michael@0: private final HomeConfig mConfig; michael@0: private ConfigLoaderCallbacks mConfigLoaderCallbacks; michael@0: michael@0: private String mInitialPanelId; michael@0: michael@0: // Cached original ViewPager background. michael@0: private final Drawable mOriginalBackground; michael@0: michael@0: // Telemetry session for current panel. michael@0: private String mCurrentPanelSession; michael@0: michael@0: // Current load state of HomePager. michael@0: private LoadState mLoadState; michael@0: michael@0: // Listens for when the current panel changes. michael@0: private OnPanelChangeListener mPanelChangedListener; michael@0: michael@0: // This is mostly used by UI tests to easily fetch michael@0: // specific list views at runtime. michael@0: static final String LIST_TAG_HISTORY = "history"; michael@0: static final String LIST_TAG_BOOKMARKS = "bookmarks"; michael@0: static final String LIST_TAG_READING_LIST = "reading_list"; michael@0: static final String LIST_TAG_TOP_SITES = "top_sites"; michael@0: static final String LIST_TAG_MOST_RECENT = "most_recent"; michael@0: static final String LIST_TAG_LAST_TABS = "last_tabs"; michael@0: static final String LIST_TAG_BROWSER_SEARCH = "browser_search"; michael@0: michael@0: public interface OnUrlOpenListener { michael@0: public enum Flags { michael@0: ALLOW_SWITCH_TO_TAB, michael@0: OPEN_WITH_INTENT michael@0: } michael@0: michael@0: public void onUrlOpen(String url, EnumSet flags); michael@0: } michael@0: michael@0: public interface OnNewTabsListener { michael@0: public void onNewTabs(String[] urls); michael@0: } michael@0: michael@0: /** michael@0: * Interface for listening into ViewPager panel changes michael@0: */ michael@0: public interface OnPanelChangeListener { michael@0: /** michael@0: * Called when a new panel is selected. michael@0: * michael@0: * @param panelId of the newly selected panel michael@0: */ michael@0: public void onPanelSelected(String panelId); michael@0: } michael@0: michael@0: interface OnTitleClickListener { michael@0: public void onTitleClicked(int index); michael@0: } michael@0: michael@0: /** michael@0: * Special type of child views that could be added as pager decorations by default. michael@0: */ michael@0: interface Decor { michael@0: public void onAddPagerView(String title); michael@0: public void removeAllPagerViews(); michael@0: public void onPageSelected(int position); michael@0: public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); michael@0: public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener); michael@0: } michael@0: michael@0: /** michael@0: * State of HomePager with respect to loading its configuration. michael@0: */ michael@0: private enum LoadState { michael@0: UNLOADED, michael@0: LOADING, michael@0: LOADED michael@0: } michael@0: michael@0: static final String CAN_LOAD_ARG = "canLoad"; michael@0: static final String PANEL_CONFIG_ARG = "panelConfig"; michael@0: michael@0: public HomePager(Context context) { michael@0: this(context, null); michael@0: } michael@0: michael@0: public HomePager(Context context, AttributeSet attrs) { michael@0: super(context, attrs); michael@0: mContext = context; michael@0: michael@0: mConfig = HomeConfig.getDefault(mContext); michael@0: mConfigLoaderCallbacks = new ConfigLoaderCallbacks(); michael@0: michael@0: mAddPanelListener = new OnAddPanelListener() { michael@0: @Override michael@0: public void onAddPanel(String title) { michael@0: if (mDecor != null) { michael@0: mDecor.onAddPagerView(title); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // This is to keep all 4 panels in memory after they are michael@0: // selected in the pager. michael@0: setOffscreenPageLimit(3); michael@0: michael@0: // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft michael@0: // keyboard. However, if there are no focusable views (e.g. an empty reading list), the michael@0: // URL bar will be refocused. Therefore, we make the HomePager container focusable to michael@0: // ensure there is always a focusable view. This would ordinarily be done via an XML michael@0: // attribute, but it is not working properly. michael@0: setFocusableInTouchMode(true); michael@0: michael@0: mOriginalBackground = getBackground(); michael@0: setOnPageChangeListener(new PageChangeListener()); michael@0: michael@0: mLoadState = LoadState.UNLOADED; michael@0: } michael@0: michael@0: @Override michael@0: public void addView(View child, int index, ViewGroup.LayoutParams params) { michael@0: if (child instanceof Decor) { michael@0: ((ViewPager.LayoutParams) params).isDecor = true; michael@0: mDecor = (Decor) child; michael@0: mTabStrip = child; michael@0: michael@0: mDecor.setOnTitleClickListener(new OnTitleClickListener() { michael@0: @Override michael@0: public void onTitleClicked(int index) { michael@0: setCurrentItem(index, true); michael@0: } michael@0: }); michael@0: } else if (child instanceof HomePagerTabStrip) { michael@0: mTabStrip = child; michael@0: } michael@0: michael@0: super.addView(child, index, params); michael@0: } michael@0: michael@0: /** michael@0: * Loads and initializes the pager. michael@0: * michael@0: * @param fm FragmentManager for the adapter michael@0: */ michael@0: public void load(LoaderManager lm, FragmentManager fm, String panelId, PropertyAnimator animator) { michael@0: mLoadState = LoadState.LOADING; michael@0: michael@0: mVisible = true; michael@0: mInitialPanelId = panelId; michael@0: michael@0: // Update the home banner message each time the HomePager is loaded. michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.update(); michael@0: } michael@0: michael@0: // Only animate on post-HC devices, when a non-null animator is given michael@0: final boolean shouldAnimate = (animator != null && Build.VERSION.SDK_INT >= 11); michael@0: michael@0: final HomeAdapter adapter = new HomeAdapter(mContext, fm); michael@0: adapter.setOnAddPanelListener(mAddPanelListener); michael@0: adapter.setCanLoadHint(!shouldAnimate); michael@0: setAdapter(adapter); michael@0: michael@0: // Don't show the tabs strip until we have the michael@0: // list of panels in place. michael@0: mTabStrip.setVisibility(View.INVISIBLE); michael@0: michael@0: // Load list of panels from configuration michael@0: lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks); michael@0: michael@0: if (shouldAnimate) { michael@0: animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { michael@0: @Override michael@0: public void onPropertyAnimationStart() { michael@0: setLayerType(View.LAYER_TYPE_HARDWARE, null); michael@0: } michael@0: michael@0: @Override michael@0: public void onPropertyAnimationEnd() { michael@0: setLayerType(View.LAYER_TYPE_NONE, null); michael@0: adapter.setCanLoadHint(true); michael@0: } michael@0: }); michael@0: michael@0: ViewHelper.setAlpha(this, 0.0f); michael@0: michael@0: animator.attach(this, michael@0: PropertyAnimator.Property.ALPHA, michael@0: 1.0f); michael@0: } michael@0: Telemetry.startUISession(TelemetryContract.Session.HOME); michael@0: } michael@0: michael@0: /** michael@0: * Removes all child fragments to free memory. michael@0: */ michael@0: public void unload() { michael@0: mVisible = false; michael@0: setAdapter(null); michael@0: mLoadState = LoadState.UNLOADED; michael@0: michael@0: // Stop UI Telemetry sessions. michael@0: stopCurrentPanelTelemetrySession(); michael@0: Telemetry.stopUISession(TelemetryContract.Session.HOME); michael@0: } michael@0: michael@0: /** michael@0: * Determines whether the pager is visible. michael@0: * michael@0: * Unlike getVisibility(), this method does not need to be called on the UI michael@0: * thread. michael@0: * michael@0: * @return Whether the pager and its fragments are loaded michael@0: */ michael@0: public boolean isVisible() { michael@0: return mVisible; michael@0: } michael@0: michael@0: @Override michael@0: public void setCurrentItem(int item, boolean smoothScroll) { michael@0: super.setCurrentItem(item, smoothScroll); michael@0: michael@0: if (mDecor != null) { michael@0: mDecor.onPageSelected(item); michael@0: } michael@0: michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.setActive(item == mDefaultPageIndex); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Shows a home panel. If the given panelId is null, michael@0: * the default panel will be shown. No action will be taken if: michael@0: * * HomePager has not loaded yet michael@0: * * Panel with the given panelId cannot be found michael@0: * michael@0: * @param panelId of the home panel to be shown. michael@0: */ michael@0: public void showPanel(String panelId) { michael@0: if (!mVisible) { michael@0: return; michael@0: } michael@0: michael@0: switch (mLoadState) { michael@0: case LOADING: michael@0: mInitialPanelId = panelId; michael@0: break; michael@0: michael@0: case LOADED: michael@0: int position = mDefaultPageIndex; michael@0: if (panelId != null) { michael@0: position = ((HomeAdapter) getAdapter()).getItemPosition(panelId); michael@0: } michael@0: michael@0: if (position > -1) { michael@0: setCurrentItem(position); michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: // Do nothing. michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onInterceptTouchEvent(MotionEvent event) { michael@0: if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { michael@0: // Drop the soft keyboard by stealing focus from the URL bar. michael@0: requestFocus(); michael@0: } michael@0: michael@0: return super.onInterceptTouchEvent(event); michael@0: } michael@0: michael@0: public void setBanner(HomeBanner banner) { michael@0: mHomeBanner = banner; michael@0: } michael@0: michael@0: @Override michael@0: public boolean dispatchTouchEvent(MotionEvent event) { michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.handleHomeTouch(event); michael@0: } michael@0: michael@0: return super.dispatchTouchEvent(event); michael@0: } michael@0: michael@0: public void onToolbarFocusChange(boolean hasFocus) { michael@0: if (mHomeBanner == null) { michael@0: return; michael@0: } michael@0: michael@0: // We should only make the banner active if the toolbar is not focused and we are on the default page michael@0: final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex; michael@0: mHomeBanner.setActive(active); michael@0: } michael@0: michael@0: private void updateUiFromConfigState(HomeConfig.State configState) { michael@0: // We only care about the adapter if HomePager is currently michael@0: // loaded, which means it's visible in the activity. michael@0: if (!mVisible) { michael@0: return; michael@0: } michael@0: michael@0: if (mDecor != null) { michael@0: mDecor.removeAllPagerViews(); michael@0: } michael@0: michael@0: final HomeAdapter adapter = (HomeAdapter) getAdapter(); michael@0: michael@0: // Disable any fragment loading until we have the initial michael@0: // panel selection done. Store previous value to restore michael@0: // it if necessary once the UI is fully updated. michael@0: final boolean canLoadHint = adapter.getCanLoadHint(); michael@0: adapter.setCanLoadHint(false); michael@0: michael@0: // Destroy any existing panels currently loaded michael@0: // in the pager. michael@0: setAdapter(null); michael@0: michael@0: // Only keep enabled panels. michael@0: final List enabledPanels = new ArrayList(); michael@0: michael@0: for (PanelConfig panelConfig : configState) { michael@0: if (!panelConfig.isDisabled()) { michael@0: enabledPanels.add(panelConfig); michael@0: } michael@0: } michael@0: michael@0: // Update the adapter with the new panel configs michael@0: adapter.update(enabledPanels); michael@0: michael@0: final int count = enabledPanels.size(); michael@0: if (count == 0) { michael@0: // Set firefox watermark as background. michael@0: setBackgroundResource(R.drawable.home_pager_empty_state); michael@0: // Hide the tab strip as there are no panels. michael@0: mTabStrip.setVisibility(View.INVISIBLE); michael@0: } else { michael@0: mTabStrip.setVisibility(View.VISIBLE); michael@0: // Restore original background. michael@0: setBackgroundDrawable(mOriginalBackground); michael@0: } michael@0: michael@0: // Re-install the adapter with the final state michael@0: // in the pager. michael@0: setAdapter(adapter); michael@0: michael@0: if (count == 0) { michael@0: mDefaultPageIndex = -1; michael@0: michael@0: // Hide the banner if there are no enabled panels. michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.setActive(false); michael@0: } michael@0: } else { michael@0: for (int i = 0; i < count; i++) { michael@0: if (enabledPanels.get(i).isDefault()) { michael@0: mDefaultPageIndex = i; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Use the default panel if the initial panel wasn't explicitly set by the michael@0: // load() caller, or if the initial panel is not found in the adapter. michael@0: final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId); michael@0: if (itemPosition > -1) { michael@0: setCurrentItem(itemPosition, false); michael@0: mInitialPanelId = null; michael@0: } else { michael@0: setCurrentItem(mDefaultPageIndex, false); michael@0: } michael@0: } michael@0: michael@0: // If the load hint was originally true, this means the pager michael@0: // is not animating and it's fine to restore the load hint back. michael@0: if (canLoadHint) { michael@0: // The selection is updated asynchronously so we need to post to michael@0: // UI thread to give the pager time to commit the new page selection michael@0: // internally and load the right initial panel. michael@0: ThreadUtils.getUiHandler().post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: adapter.setCanLoadHint(true); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: public void setOnPanelChangeListener(OnPanelChangeListener listener) { michael@0: mPanelChangedListener = listener; michael@0: } michael@0: michael@0: /** michael@0: * Notify listeners of newly selected panel. michael@0: * michael@0: * @param position of the newly selected panel michael@0: */ michael@0: private void notifyPanelSelected(int position) { michael@0: if (mDecor != null) { michael@0: mDecor.onPageSelected(position); michael@0: } michael@0: michael@0: if (mPanelChangedListener != null) { michael@0: final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); michael@0: mPanelChangedListener.onPanelSelected(panelId); michael@0: } michael@0: } michael@0: michael@0: private class ConfigLoaderCallbacks implements LoaderCallbacks { michael@0: @Override michael@0: public Loader onCreateLoader(int id, Bundle args) { michael@0: return new HomeConfigLoader(mContext, mConfig); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoadFinished(Loader loader, HomeConfig.State configState) { michael@0: mLoadState = LoadState.LOADED; michael@0: updateUiFromConfigState(configState); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoaderReset(Loader loader) { michael@0: mLoadState = LoadState.UNLOADED; michael@0: } michael@0: } michael@0: michael@0: private class PageChangeListener implements ViewPager.OnPageChangeListener { michael@0: @Override michael@0: public void onPageSelected(int position) { michael@0: notifyPanelSelected(position); michael@0: michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.setActive(position == mDefaultPageIndex); michael@0: } michael@0: michael@0: // Start a UI telemetry session for the newly selected panel. michael@0: final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); michael@0: startNewPanelTelemetrySession(newPanelId); michael@0: } michael@0: michael@0: @Override michael@0: public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { michael@0: if (mDecor != null) { michael@0: mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); michael@0: } michael@0: michael@0: if (mHomeBanner != null) { michael@0: mHomeBanner.setScrollingPages(positionOffsetPixels != 0); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onPageScrollStateChanged(int state) { } michael@0: } michael@0: michael@0: /** michael@0: * Start UI telemetry session for the a panel. michael@0: * If there is currently a session open for a panel, michael@0: * it will be stopped before a new one is started. michael@0: * michael@0: * @param panelId of panel to start a session for michael@0: */ michael@0: private void startNewPanelTelemetrySession(String panelId) { michael@0: // Stop the current panel's session if we have one. michael@0: stopCurrentPanelTelemetrySession(); michael@0: michael@0: mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL + panelId; michael@0: Telemetry.startUISession(mCurrentPanelSession); michael@0: } michael@0: michael@0: /** michael@0: * Stop the current panel telemetry session if one exists. michael@0: */ michael@0: private void stopCurrentPanelTelemetrySession() { michael@0: if (mCurrentPanelSession != null) { michael@0: Telemetry.stopUISession(mCurrentPanelSession); michael@0: mCurrentPanelSession = null; michael@0: } michael@0: } michael@0: }