1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/HomePager.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,525 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.home; 1.10 + 1.11 +import java.util.ArrayList; 1.12 +import java.util.EnumSet; 1.13 +import java.util.List; 1.14 + 1.15 +import org.mozilla.gecko.R; 1.16 +import org.mozilla.gecko.Telemetry; 1.17 +import org.mozilla.gecko.TelemetryContract; 1.18 +import org.mozilla.gecko.animation.PropertyAnimator; 1.19 +import org.mozilla.gecko.animation.ViewHelper; 1.20 +import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener; 1.21 +import org.mozilla.gecko.home.HomeConfig.PanelConfig; 1.22 +import org.mozilla.gecko.util.ThreadUtils; 1.23 + 1.24 +import android.content.Context; 1.25 +import android.graphics.drawable.Drawable; 1.26 +import android.os.Build; 1.27 +import android.os.Bundle; 1.28 +import android.support.v4.app.FragmentManager; 1.29 +import android.support.v4.app.LoaderManager; 1.30 +import android.support.v4.app.LoaderManager.LoaderCallbacks; 1.31 +import android.support.v4.content.Loader; 1.32 +import android.support.v4.view.ViewPager; 1.33 +import android.util.AttributeSet; 1.34 +import android.view.MotionEvent; 1.35 +import android.view.View; 1.36 +import android.view.ViewGroup; 1.37 + 1.38 +public class HomePager extends ViewPager { 1.39 + private static final int LOADER_ID_CONFIG = 0; 1.40 + 1.41 + private final Context mContext; 1.42 + private volatile boolean mVisible; 1.43 + private Decor mDecor; 1.44 + private View mTabStrip; 1.45 + private HomeBanner mHomeBanner; 1.46 + private int mDefaultPageIndex = -1; 1.47 + 1.48 + private final OnAddPanelListener mAddPanelListener; 1.49 + 1.50 + private final HomeConfig mConfig; 1.51 + private ConfigLoaderCallbacks mConfigLoaderCallbacks; 1.52 + 1.53 + private String mInitialPanelId; 1.54 + 1.55 + // Cached original ViewPager background. 1.56 + private final Drawable mOriginalBackground; 1.57 + 1.58 + // Telemetry session for current panel. 1.59 + private String mCurrentPanelSession; 1.60 + 1.61 + // Current load state of HomePager. 1.62 + private LoadState mLoadState; 1.63 + 1.64 + // Listens for when the current panel changes. 1.65 + private OnPanelChangeListener mPanelChangedListener; 1.66 + 1.67 + // This is mostly used by UI tests to easily fetch 1.68 + // specific list views at runtime. 1.69 + static final String LIST_TAG_HISTORY = "history"; 1.70 + static final String LIST_TAG_BOOKMARKS = "bookmarks"; 1.71 + static final String LIST_TAG_READING_LIST = "reading_list"; 1.72 + static final String LIST_TAG_TOP_SITES = "top_sites"; 1.73 + static final String LIST_TAG_MOST_RECENT = "most_recent"; 1.74 + static final String LIST_TAG_LAST_TABS = "last_tabs"; 1.75 + static final String LIST_TAG_BROWSER_SEARCH = "browser_search"; 1.76 + 1.77 + public interface OnUrlOpenListener { 1.78 + public enum Flags { 1.79 + ALLOW_SWITCH_TO_TAB, 1.80 + OPEN_WITH_INTENT 1.81 + } 1.82 + 1.83 + public void onUrlOpen(String url, EnumSet<Flags> flags); 1.84 + } 1.85 + 1.86 + public interface OnNewTabsListener { 1.87 + public void onNewTabs(String[] urls); 1.88 + } 1.89 + 1.90 + /** 1.91 + * Interface for listening into ViewPager panel changes 1.92 + */ 1.93 + public interface OnPanelChangeListener { 1.94 + /** 1.95 + * Called when a new panel is selected. 1.96 + * 1.97 + * @param panelId of the newly selected panel 1.98 + */ 1.99 + public void onPanelSelected(String panelId); 1.100 + } 1.101 + 1.102 + interface OnTitleClickListener { 1.103 + public void onTitleClicked(int index); 1.104 + } 1.105 + 1.106 + /** 1.107 + * Special type of child views that could be added as pager decorations by default. 1.108 + */ 1.109 + interface Decor { 1.110 + public void onAddPagerView(String title); 1.111 + public void removeAllPagerViews(); 1.112 + public void onPageSelected(int position); 1.113 + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 1.114 + public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener); 1.115 + } 1.116 + 1.117 + /** 1.118 + * State of HomePager with respect to loading its configuration. 1.119 + */ 1.120 + private enum LoadState { 1.121 + UNLOADED, 1.122 + LOADING, 1.123 + LOADED 1.124 + } 1.125 + 1.126 + static final String CAN_LOAD_ARG = "canLoad"; 1.127 + static final String PANEL_CONFIG_ARG = "panelConfig"; 1.128 + 1.129 + public HomePager(Context context) { 1.130 + this(context, null); 1.131 + } 1.132 + 1.133 + public HomePager(Context context, AttributeSet attrs) { 1.134 + super(context, attrs); 1.135 + mContext = context; 1.136 + 1.137 + mConfig = HomeConfig.getDefault(mContext); 1.138 + mConfigLoaderCallbacks = new ConfigLoaderCallbacks(); 1.139 + 1.140 + mAddPanelListener = new OnAddPanelListener() { 1.141 + @Override 1.142 + public void onAddPanel(String title) { 1.143 + if (mDecor != null) { 1.144 + mDecor.onAddPagerView(title); 1.145 + } 1.146 + } 1.147 + }; 1.148 + 1.149 + // This is to keep all 4 panels in memory after they are 1.150 + // selected in the pager. 1.151 + setOffscreenPageLimit(3); 1.152 + 1.153 + // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft 1.154 + // keyboard. However, if there are no focusable views (e.g. an empty reading list), the 1.155 + // URL bar will be refocused. Therefore, we make the HomePager container focusable to 1.156 + // ensure there is always a focusable view. This would ordinarily be done via an XML 1.157 + // attribute, but it is not working properly. 1.158 + setFocusableInTouchMode(true); 1.159 + 1.160 + mOriginalBackground = getBackground(); 1.161 + setOnPageChangeListener(new PageChangeListener()); 1.162 + 1.163 + mLoadState = LoadState.UNLOADED; 1.164 + } 1.165 + 1.166 + @Override 1.167 + public void addView(View child, int index, ViewGroup.LayoutParams params) { 1.168 + if (child instanceof Decor) { 1.169 + ((ViewPager.LayoutParams) params).isDecor = true; 1.170 + mDecor = (Decor) child; 1.171 + mTabStrip = child; 1.172 + 1.173 + mDecor.setOnTitleClickListener(new OnTitleClickListener() { 1.174 + @Override 1.175 + public void onTitleClicked(int index) { 1.176 + setCurrentItem(index, true); 1.177 + } 1.178 + }); 1.179 + } else if (child instanceof HomePagerTabStrip) { 1.180 + mTabStrip = child; 1.181 + } 1.182 + 1.183 + super.addView(child, index, params); 1.184 + } 1.185 + 1.186 + /** 1.187 + * Loads and initializes the pager. 1.188 + * 1.189 + * @param fm FragmentManager for the adapter 1.190 + */ 1.191 + public void load(LoaderManager lm, FragmentManager fm, String panelId, PropertyAnimator animator) { 1.192 + mLoadState = LoadState.LOADING; 1.193 + 1.194 + mVisible = true; 1.195 + mInitialPanelId = panelId; 1.196 + 1.197 + // Update the home banner message each time the HomePager is loaded. 1.198 + if (mHomeBanner != null) { 1.199 + mHomeBanner.update(); 1.200 + } 1.201 + 1.202 + // Only animate on post-HC devices, when a non-null animator is given 1.203 + final boolean shouldAnimate = (animator != null && Build.VERSION.SDK_INT >= 11); 1.204 + 1.205 + final HomeAdapter adapter = new HomeAdapter(mContext, fm); 1.206 + adapter.setOnAddPanelListener(mAddPanelListener); 1.207 + adapter.setCanLoadHint(!shouldAnimate); 1.208 + setAdapter(adapter); 1.209 + 1.210 + // Don't show the tabs strip until we have the 1.211 + // list of panels in place. 1.212 + mTabStrip.setVisibility(View.INVISIBLE); 1.213 + 1.214 + // Load list of panels from configuration 1.215 + lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks); 1.216 + 1.217 + if (shouldAnimate) { 1.218 + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { 1.219 + @Override 1.220 + public void onPropertyAnimationStart() { 1.221 + setLayerType(View.LAYER_TYPE_HARDWARE, null); 1.222 + } 1.223 + 1.224 + @Override 1.225 + public void onPropertyAnimationEnd() { 1.226 + setLayerType(View.LAYER_TYPE_NONE, null); 1.227 + adapter.setCanLoadHint(true); 1.228 + } 1.229 + }); 1.230 + 1.231 + ViewHelper.setAlpha(this, 0.0f); 1.232 + 1.233 + animator.attach(this, 1.234 + PropertyAnimator.Property.ALPHA, 1.235 + 1.0f); 1.236 + } 1.237 + Telemetry.startUISession(TelemetryContract.Session.HOME); 1.238 + } 1.239 + 1.240 + /** 1.241 + * Removes all child fragments to free memory. 1.242 + */ 1.243 + public void unload() { 1.244 + mVisible = false; 1.245 + setAdapter(null); 1.246 + mLoadState = LoadState.UNLOADED; 1.247 + 1.248 + // Stop UI Telemetry sessions. 1.249 + stopCurrentPanelTelemetrySession(); 1.250 + Telemetry.stopUISession(TelemetryContract.Session.HOME); 1.251 + } 1.252 + 1.253 + /** 1.254 + * Determines whether the pager is visible. 1.255 + * 1.256 + * Unlike getVisibility(), this method does not need to be called on the UI 1.257 + * thread. 1.258 + * 1.259 + * @return Whether the pager and its fragments are loaded 1.260 + */ 1.261 + public boolean isVisible() { 1.262 + return mVisible; 1.263 + } 1.264 + 1.265 + @Override 1.266 + public void setCurrentItem(int item, boolean smoothScroll) { 1.267 + super.setCurrentItem(item, smoothScroll); 1.268 + 1.269 + if (mDecor != null) { 1.270 + mDecor.onPageSelected(item); 1.271 + } 1.272 + 1.273 + if (mHomeBanner != null) { 1.274 + mHomeBanner.setActive(item == mDefaultPageIndex); 1.275 + } 1.276 + } 1.277 + 1.278 + /** 1.279 + * Shows a home panel. If the given panelId is null, 1.280 + * the default panel will be shown. No action will be taken if: 1.281 + * * HomePager has not loaded yet 1.282 + * * Panel with the given panelId cannot be found 1.283 + * 1.284 + * @param panelId of the home panel to be shown. 1.285 + */ 1.286 + public void showPanel(String panelId) { 1.287 + if (!mVisible) { 1.288 + return; 1.289 + } 1.290 + 1.291 + switch (mLoadState) { 1.292 + case LOADING: 1.293 + mInitialPanelId = panelId; 1.294 + break; 1.295 + 1.296 + case LOADED: 1.297 + int position = mDefaultPageIndex; 1.298 + if (panelId != null) { 1.299 + position = ((HomeAdapter) getAdapter()).getItemPosition(panelId); 1.300 + } 1.301 + 1.302 + if (position > -1) { 1.303 + setCurrentItem(position); 1.304 + } 1.305 + break; 1.306 + 1.307 + default: 1.308 + // Do nothing. 1.309 + } 1.310 + } 1.311 + 1.312 + @Override 1.313 + public boolean onInterceptTouchEvent(MotionEvent event) { 1.314 + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1.315 + // Drop the soft keyboard by stealing focus from the URL bar. 1.316 + requestFocus(); 1.317 + } 1.318 + 1.319 + return super.onInterceptTouchEvent(event); 1.320 + } 1.321 + 1.322 + public void setBanner(HomeBanner banner) { 1.323 + mHomeBanner = banner; 1.324 + } 1.325 + 1.326 + @Override 1.327 + public boolean dispatchTouchEvent(MotionEvent event) { 1.328 + if (mHomeBanner != null) { 1.329 + mHomeBanner.handleHomeTouch(event); 1.330 + } 1.331 + 1.332 + return super.dispatchTouchEvent(event); 1.333 + } 1.334 + 1.335 + public void onToolbarFocusChange(boolean hasFocus) { 1.336 + if (mHomeBanner == null) { 1.337 + return; 1.338 + } 1.339 + 1.340 + // We should only make the banner active if the toolbar is not focused and we are on the default page 1.341 + final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex; 1.342 + mHomeBanner.setActive(active); 1.343 + } 1.344 + 1.345 + private void updateUiFromConfigState(HomeConfig.State configState) { 1.346 + // We only care about the adapter if HomePager is currently 1.347 + // loaded, which means it's visible in the activity. 1.348 + if (!mVisible) { 1.349 + return; 1.350 + } 1.351 + 1.352 + if (mDecor != null) { 1.353 + mDecor.removeAllPagerViews(); 1.354 + } 1.355 + 1.356 + final HomeAdapter adapter = (HomeAdapter) getAdapter(); 1.357 + 1.358 + // Disable any fragment loading until we have the initial 1.359 + // panel selection done. Store previous value to restore 1.360 + // it if necessary once the UI is fully updated. 1.361 + final boolean canLoadHint = adapter.getCanLoadHint(); 1.362 + adapter.setCanLoadHint(false); 1.363 + 1.364 + // Destroy any existing panels currently loaded 1.365 + // in the pager. 1.366 + setAdapter(null); 1.367 + 1.368 + // Only keep enabled panels. 1.369 + final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>(); 1.370 + 1.371 + for (PanelConfig panelConfig : configState) { 1.372 + if (!panelConfig.isDisabled()) { 1.373 + enabledPanels.add(panelConfig); 1.374 + } 1.375 + } 1.376 + 1.377 + // Update the adapter with the new panel configs 1.378 + adapter.update(enabledPanels); 1.379 + 1.380 + final int count = enabledPanels.size(); 1.381 + if (count == 0) { 1.382 + // Set firefox watermark as background. 1.383 + setBackgroundResource(R.drawable.home_pager_empty_state); 1.384 + // Hide the tab strip as there are no panels. 1.385 + mTabStrip.setVisibility(View.INVISIBLE); 1.386 + } else { 1.387 + mTabStrip.setVisibility(View.VISIBLE); 1.388 + // Restore original background. 1.389 + setBackgroundDrawable(mOriginalBackground); 1.390 + } 1.391 + 1.392 + // Re-install the adapter with the final state 1.393 + // in the pager. 1.394 + setAdapter(adapter); 1.395 + 1.396 + if (count == 0) { 1.397 + mDefaultPageIndex = -1; 1.398 + 1.399 + // Hide the banner if there are no enabled panels. 1.400 + if (mHomeBanner != null) { 1.401 + mHomeBanner.setActive(false); 1.402 + } 1.403 + } else { 1.404 + for (int i = 0; i < count; i++) { 1.405 + if (enabledPanels.get(i).isDefault()) { 1.406 + mDefaultPageIndex = i; 1.407 + break; 1.408 + } 1.409 + } 1.410 + 1.411 + // Use the default panel if the initial panel wasn't explicitly set by the 1.412 + // load() caller, or if the initial panel is not found in the adapter. 1.413 + final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId); 1.414 + if (itemPosition > -1) { 1.415 + setCurrentItem(itemPosition, false); 1.416 + mInitialPanelId = null; 1.417 + } else { 1.418 + setCurrentItem(mDefaultPageIndex, false); 1.419 + } 1.420 + } 1.421 + 1.422 + // If the load hint was originally true, this means the pager 1.423 + // is not animating and it's fine to restore the load hint back. 1.424 + if (canLoadHint) { 1.425 + // The selection is updated asynchronously so we need to post to 1.426 + // UI thread to give the pager time to commit the new page selection 1.427 + // internally and load the right initial panel. 1.428 + ThreadUtils.getUiHandler().post(new Runnable() { 1.429 + @Override 1.430 + public void run() { 1.431 + adapter.setCanLoadHint(true); 1.432 + } 1.433 + }); 1.434 + } 1.435 + } 1.436 + 1.437 + public void setOnPanelChangeListener(OnPanelChangeListener listener) { 1.438 + mPanelChangedListener = listener; 1.439 + } 1.440 + 1.441 + /** 1.442 + * Notify listeners of newly selected panel. 1.443 + * 1.444 + * @param position of the newly selected panel 1.445 + */ 1.446 + private void notifyPanelSelected(int position) { 1.447 + if (mDecor != null) { 1.448 + mDecor.onPageSelected(position); 1.449 + } 1.450 + 1.451 + if (mPanelChangedListener != null) { 1.452 + final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); 1.453 + mPanelChangedListener.onPanelSelected(panelId); 1.454 + } 1.455 + } 1.456 + 1.457 + private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> { 1.458 + @Override 1.459 + public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) { 1.460 + return new HomeConfigLoader(mContext, mConfig); 1.461 + } 1.462 + 1.463 + @Override 1.464 + public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) { 1.465 + mLoadState = LoadState.LOADED; 1.466 + updateUiFromConfigState(configState); 1.467 + } 1.468 + 1.469 + @Override 1.470 + public void onLoaderReset(Loader<HomeConfig.State> loader) { 1.471 + mLoadState = LoadState.UNLOADED; 1.472 + } 1.473 + } 1.474 + 1.475 + private class PageChangeListener implements ViewPager.OnPageChangeListener { 1.476 + @Override 1.477 + public void onPageSelected(int position) { 1.478 + notifyPanelSelected(position); 1.479 + 1.480 + if (mHomeBanner != null) { 1.481 + mHomeBanner.setActive(position == mDefaultPageIndex); 1.482 + } 1.483 + 1.484 + // Start a UI telemetry session for the newly selected panel. 1.485 + final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); 1.486 + startNewPanelTelemetrySession(newPanelId); 1.487 + } 1.488 + 1.489 + @Override 1.490 + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 1.491 + if (mDecor != null) { 1.492 + mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); 1.493 + } 1.494 + 1.495 + if (mHomeBanner != null) { 1.496 + mHomeBanner.setScrollingPages(positionOffsetPixels != 0); 1.497 + } 1.498 + } 1.499 + 1.500 + @Override 1.501 + public void onPageScrollStateChanged(int state) { } 1.502 + } 1.503 + 1.504 + /** 1.505 + * Start UI telemetry session for the a panel. 1.506 + * If there is currently a session open for a panel, 1.507 + * it will be stopped before a new one is started. 1.508 + * 1.509 + * @param panelId of panel to start a session for 1.510 + */ 1.511 + private void startNewPanelTelemetrySession(String panelId) { 1.512 + // Stop the current panel's session if we have one. 1.513 + stopCurrentPanelTelemetrySession(); 1.514 + 1.515 + mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL + panelId; 1.516 + Telemetry.startUISession(mCurrentPanelSession); 1.517 + } 1.518 + 1.519 + /** 1.520 + * Stop the current panel telemetry session if one exists. 1.521 + */ 1.522 + private void stopCurrentPanelTelemetrySession() { 1.523 + if (mCurrentPanelSession != null) { 1.524 + Telemetry.stopUISession(mCurrentPanelSession); 1.525 + mCurrentPanelSession = null; 1.526 + } 1.527 + } 1.528 +}