1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko.home; 7 8 import java.util.ArrayList; 9 import java.util.EnumSet; 10 import java.util.List; 11 12 import org.mozilla.gecko.AppConstants.Versions; 13 import org.mozilla.gecko.R; 14 import org.mozilla.gecko.Telemetry; 15 import org.mozilla.gecko.TelemetryContract; 16 import org.mozilla.gecko.activitystream.ActivityStream; 17 import org.mozilla.gecko.animation.PropertyAnimator; 18 import org.mozilla.gecko.animation.ViewHelper; 19 import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener; 20 import org.mozilla.gecko.home.HomeConfig.PanelConfig; 21 import org.mozilla.gecko.util.ThreadUtils; 22 23 import android.content.Context; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.os.Bundle; 27 import android.support.v4.app.FragmentManager; 28 import android.support.v4.app.LoaderManager; 29 import android.support.v4.app.LoaderManager.LoaderCallbacks; 30 import android.support.v4.content.Loader; 31 import android.util.AttributeSet; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.ViewGroup; 35 36 import com.booking.rtlviewpager.RtlViewPager; 37 38 public class HomePager extends RtlViewPager implements HomeScreen { 39 40 @Override requestFocus(int direction, Rect previouslyFocusedRect)41 public boolean requestFocus(int direction, Rect previouslyFocusedRect) { 42 return super.requestFocus(direction, previouslyFocusedRect); 43 } 44 45 private static final int LOADER_ID_CONFIG = 0; 46 47 private final Context mContext; 48 private volatile boolean mVisible; 49 private Decor mDecor; 50 private View mTabStrip; 51 private HomeBanner mHomeBanner; 52 private int mDefaultPageIndex = -1; 53 54 private final OnAddPanelListener mAddPanelListener; 55 56 private final HomeConfig mConfig; 57 private final ConfigLoaderCallbacks mConfigLoaderCallbacks; 58 59 private String mInitialPanelId; 60 private Bundle mRestoreData; 61 62 // Cached original ViewPager background. 63 private final Drawable mOriginalBackground; 64 65 // Telemetry session for current panel. 66 private TelemetryContract.Session mCurrentPanelSession; 67 private String mCurrentPanelSessionSuffix; 68 69 // Current load state of HomePager. 70 private LoadState mLoadState; 71 72 // Listens for when the current panel changes. 73 private OnPanelChangeListener mPanelChangedListener; 74 75 private HomeFragment.PanelStateChangeListener mPanelStateChangeListener; 76 77 // This is mostly used by UI tests to easily fetch 78 // specific list views at runtime. 79 public static final String LIST_TAG_HISTORY = "history"; 80 public static final String LIST_TAG_BOOKMARKS = "bookmarks"; 81 public static final String LIST_TAG_TOP_SITES = "top_sites"; 82 public static final String LIST_TAG_RECENT_TABS = "recent_tabs"; 83 public static final String LIST_TAG_BROWSER_SEARCH = "browser_search"; 84 public static final String LIST_TAG_REMOTE_TABS = "remote_tabs"; 85 86 public interface OnUrlOpenListener { 87 public enum Flags { 88 ALLOW_SWITCH_TO_TAB, 89 OPEN_WITH_INTENT, 90 /** 91 * Ensure that the raw URL is opened. If not set, then the reader view version of the page 92 * might be opened if the URL is stored as an offline reader-view bookmark. 93 */ 94 NO_READER_VIEW 95 } 96 onUrlOpen(String url, EnumSet<Flags> flags)97 void onUrlOpen(String url, EnumSet<Flags> flags); onUrlOpenWithReferrer(String url, String referrerUri, EnumSet<Flags> flags)98 void onUrlOpenWithReferrer(String url, String referrerUri, EnumSet<Flags> flags); 99 } 100 101 /** 102 * Interface for requesting a new tab be opened in the background. 103 * <p> 104 * This is the <code>HomeFragment</code> equivalent of opening a new tab by 105 * long clicking a link and selecting the "Open new [private] tab" context 106 * menu option. 107 */ 108 public interface OnUrlOpenInBackgroundListener { 109 public enum Flags { 110 PRIVATE, 111 } 112 113 /** 114 * Open a new tab with the given URL 115 * 116 * @param url to open. 117 * @param flags to open new tab with. 118 */ onUrlOpenInBackground(String url, EnumSet<Flags> flags)119 public void onUrlOpenInBackground(String url, EnumSet<Flags> flags); onUrlOpenInBackgroundWithReferrer(String url, String referrerUri, EnumSet<Flags> flags)120 void onUrlOpenInBackgroundWithReferrer(String url, String referrerUri, EnumSet<Flags> flags); 121 } 122 123 /** 124 * Special type of child views that could be added as pager decorations by default. 125 */ 126 public interface Decor { onAddPagerView(String title)127 void onAddPagerView(String title); removeAllPagerViews()128 void removeAllPagerViews(); onPageSelected(int position)129 void onPageSelected(int position); onPageScrolled(int position, float positionOffset, int positionOffsetPixels)130 void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener)131 void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener); 132 } 133 134 /** 135 * State of HomePager with respect to loading its configuration. 136 */ 137 private enum LoadState { 138 UNLOADED, 139 LOADING, 140 LOADED 141 } 142 143 public static final String CAN_LOAD_ARG = "canLoad"; 144 public static final String PANEL_CONFIG_ARG = "panelConfig"; 145 HomePager(Context context)146 public HomePager(Context context) { 147 this(context, null); 148 } 149 HomePager(Context context, AttributeSet attrs)150 public HomePager(Context context, AttributeSet attrs) { 151 super(context, attrs); 152 mContext = context; 153 154 mConfig = HomeConfig.getDefault(mContext); 155 mConfigLoaderCallbacks = new ConfigLoaderCallbacks(); 156 157 mAddPanelListener = new OnAddPanelListener() { 158 @Override 159 public void onAddPanel(String title) { 160 if (mDecor != null) { 161 mDecor.onAddPagerView(title); 162 } 163 } 164 }; 165 166 // This is to keep all 4 panels in memory after they are 167 // selected in the pager. 168 setOffscreenPageLimit(3); 169 170 // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft 171 // keyboard. However, if there are no focusable views (e.g. an empty reading list), the 172 // URL bar will be refocused. Therefore, we make the HomePager container focusable to 173 // ensure there is always a focusable view. This would ordinarily be done via an XML 174 // attribute, but it is not working properly. 175 setFocusableInTouchMode(true); 176 177 mOriginalBackground = getBackground(); 178 addOnPageChangeListener(new PageChangeListener()); 179 180 mLoadState = LoadState.UNLOADED; 181 } 182 183 @Override addView(View child, int index, ViewGroup.LayoutParams params)184 public void addView(View child, int index, ViewGroup.LayoutParams params) { 185 if (child instanceof Decor) { 186 ((RtlViewPager.LayoutParams) params).isDecor = true; 187 mDecor = (Decor) child; 188 mTabStrip = child; 189 190 mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() { 191 @Override 192 public void onTitleClicked(int index) { 193 setCurrentItem(index, true); 194 } 195 }); 196 } 197 198 super.addView(child, index, params); 199 } 200 201 /** 202 * Loads and initializes the pager. 203 * 204 * @param fm FragmentManager for the adapter 205 */ 206 @Override load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator)207 public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator) { 208 mLoadState = LoadState.LOADING; 209 210 mVisible = true; 211 mInitialPanelId = panelId; 212 mRestoreData = restoreData; 213 214 // Update the home banner message each time the HomePager is loaded. 215 if (mHomeBanner != null) { 216 mHomeBanner.update(); 217 } 218 219 // Only animate on post-HC devices, when a non-null animator is given 220 final boolean shouldAnimate = animator != null; 221 222 final HomeAdapter adapter = new HomeAdapter(mContext, fm); 223 adapter.setOnAddPanelListener(mAddPanelListener); 224 adapter.setPanelStateChangeListener(mPanelStateChangeListener); 225 adapter.setCanLoadHint(true); 226 setAdapter(adapter); 227 228 // Don't show the tabs strip until we have the 229 // list of panels in place. 230 mTabStrip.setVisibility(View.INVISIBLE); 231 232 // If HomeConfigLoader already exist and there's no restoreData(for bookmark's parentStack), 233 // call forceLoad() to trigger updateUiFromConfigState() and reset HomePager's adapter. 234 if (lm.getLoader(LOADER_ID_CONFIG) != null && restoreData == null) { 235 lm.getLoader(LOADER_ID_CONFIG).forceLoad(); 236 } else { 237 // Load list of panels from configuration 238 lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks); 239 } 240 241 if (shouldAnimate) { 242 animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { 243 @Override 244 public void onPropertyAnimationStart() { 245 setLayerType(View.LAYER_TYPE_HARDWARE, null); 246 } 247 248 @Override 249 public void onPropertyAnimationEnd() { 250 setLayerType(View.LAYER_TYPE_NONE, null); 251 } 252 }); 253 254 ViewHelper.setAlpha(this, 0.0f); 255 256 animator.attach(this, 257 PropertyAnimator.Property.ALPHA, 258 1.0f); 259 } 260 } 261 262 /** 263 * Removes all child fragments to free memory. 264 */ 265 @Override unload()266 public void unload() { 267 mVisible = false; 268 setAdapter(null); 269 mLoadState = LoadState.UNLOADED; 270 271 // Stop UI Telemetry sessions. 272 stopCurrentPanelTelemetrySession(); 273 } 274 275 /** 276 * Determines whether the pager is visible. 277 * 278 * Unlike getVisibility(), this method does not need to be called on the UI 279 * thread. 280 * 281 * @return Whether the pager and its fragments are loaded 282 */ isVisible()283 public boolean isVisible() { 284 return mVisible; 285 } 286 287 @Override setCurrentItem(int item, boolean smoothScroll)288 public void setCurrentItem(int item, boolean smoothScroll) { 289 super.setCurrentItem(item, smoothScroll); 290 291 if (mDecor != null) { 292 mDecor.onPageSelected(item); 293 } 294 295 if (mHomeBanner != null) { 296 mHomeBanner.setActive(item == mDefaultPageIndex); 297 } 298 } 299 restorePanelData(int item, Bundle data)300 private void restorePanelData(int item, Bundle data) { 301 ((HomeAdapter) getAdapter()).setRestoreData(item, data); 302 } 303 304 /** 305 * Shows a home panel. If the given panelId is null, 306 * the default panel will be shown. No action will be taken if: 307 * * HomePager has not loaded yet 308 * * Panel with the given panelId cannot be found 309 * 310 * If you're trying to open a built-in panel, consider loading the panel url directly with 311 * {@link org.mozilla.gecko.AboutPages#getURLForBuiltinPanelType(HomeConfig.PanelType)}. 312 * 313 * @param panelId of the home panel to be shown. 314 */ 315 @Override showPanel(String panelId, Bundle restoreData)316 public void showPanel(String panelId, Bundle restoreData) { 317 if (!mVisible) { 318 return; 319 } 320 321 switch (mLoadState) { 322 case LOADING: 323 mInitialPanelId = panelId; 324 mRestoreData = restoreData; 325 break; 326 327 case LOADED: 328 int position = mDefaultPageIndex; 329 if (panelId != null) { 330 position = ((HomeAdapter) getAdapter()).getItemPosition(panelId); 331 } 332 333 if (position > -1) { 334 setCurrentItem(position); 335 if (restoreData != null) { 336 restorePanelData(position, restoreData); 337 } 338 } 339 break; 340 341 default: 342 // Do nothing. 343 } 344 } 345 346 @Override onInterceptTouchEvent(MotionEvent event)347 public boolean onInterceptTouchEvent(MotionEvent event) { 348 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 349 // Drop the soft keyboard by stealing focus from the URL bar. 350 requestFocus(); 351 } 352 353 return super.onInterceptTouchEvent(event); 354 } 355 setBanner(HomeBanner banner)356 public void setBanner(HomeBanner banner) { 357 mHomeBanner = banner; 358 } 359 360 @Override dispatchTouchEvent(MotionEvent event)361 public boolean dispatchTouchEvent(MotionEvent event) { 362 if (mHomeBanner != null) { 363 mHomeBanner.handleHomeTouch(event); 364 } 365 366 return super.dispatchTouchEvent(event); 367 } 368 369 @Override onToolbarFocusChange(boolean hasFocus)370 public void onToolbarFocusChange(boolean hasFocus) { 371 if (mHomeBanner == null) { 372 return; 373 } 374 375 // We should only make the banner active if the toolbar is not focused and we are on the default page 376 final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex; 377 mHomeBanner.setActive(active); 378 } 379 updateUiFromConfigState(HomeConfig.State configState)380 private void updateUiFromConfigState(HomeConfig.State configState) { 381 // We only care about the adapter if HomePager is currently 382 // loaded, which means it's visible in the activity. 383 if (!mVisible) { 384 return; 385 } 386 387 if (mDecor != null) { 388 mDecor.removeAllPagerViews(); 389 } 390 391 final HomeAdapter adapter = (HomeAdapter) getAdapter(); 392 393 // Disable any fragment loading until we have the initial 394 // panel selection done. 395 adapter.setCanLoadHint(false); 396 397 // Destroy any existing panels currently loaded 398 // in the pager. 399 setAdapter(null); 400 401 // Only keep enabled panels. 402 final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>(); 403 404 for (PanelConfig panelConfig : configState) { 405 if (!panelConfig.isDisabled()) { 406 enabledPanels.add(panelConfig); 407 } 408 } 409 410 // Update the adapter with the new panel configs 411 adapter.update(enabledPanels); 412 413 final int count = enabledPanels.size(); 414 if (count == 0) { 415 // Set firefox watermark as background. 416 setBackgroundResource(R.drawable.home_pager_empty_state); 417 // Hide the tab strip as there are no panels. 418 mTabStrip.setVisibility(View.INVISIBLE); 419 } else { 420 mTabStrip.setVisibility(View.VISIBLE); 421 // Restore original background. 422 setBackgroundDrawable(mOriginalBackground); 423 } 424 425 // Re-install the adapter with the final state 426 // in the pager. 427 setAdapter(adapter); 428 429 if (count == 0) { 430 mDefaultPageIndex = -1; 431 432 // Hide the banner if there are no enabled panels. 433 if (mHomeBanner != null) { 434 mHomeBanner.setActive(false); 435 } 436 } else { 437 for (int i = 0; i < count; i++) { 438 if (enabledPanels.get(i).isDefault()) { 439 mDefaultPageIndex = i; 440 break; 441 } 442 } 443 444 // Use the default panel if the initial panel wasn't explicitly set by the 445 // load() caller, or if the initial panel is not found in the adapter. 446 final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId); 447 if (itemPosition > -1) { 448 setCurrentItem(itemPosition, false); 449 if (mRestoreData != null) { 450 restorePanelData(itemPosition, mRestoreData); 451 mRestoreData = null; // Release data since it's no longer needed 452 } 453 mInitialPanelId = null; 454 } else { 455 setCurrentItem(mDefaultPageIndex, false); 456 } 457 } 458 459 // The selection is updated asynchronously so we need to post to 460 // UI thread to give the pager time to commit the new page selection 461 // internally and load the right initial panel. 462 ThreadUtils.getUiHandler().post(new Runnable() { 463 @Override 464 public void run() { 465 adapter.setCanLoadHint(true); 466 } 467 }); 468 469 // We need to fire telemetry on the initial load: we will subsequently send telemetry whenever 470 // the user switches between homepanels, but the first load doesn't involve any switching hence 471 // we need to send telemetry now: 472 final String panelType = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(mDefaultPageIndex); 473 startNewPanelTelemetrySession(panelType); 474 } 475 476 @Override setOnPanelChangeListener(OnPanelChangeListener listener)477 public void setOnPanelChangeListener(OnPanelChangeListener listener) { 478 mPanelChangedListener = listener; 479 } 480 481 @Override setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener)482 public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) { 483 mPanelStateChangeListener = listener; 484 485 HomeAdapter adapter = (HomeAdapter) getAdapter(); 486 if (adapter != null) { 487 adapter.setPanelStateChangeListener(listener); 488 } 489 } 490 491 /** 492 * Notify listeners of newly selected panel. 493 * 494 * @param position of the newly selected panel 495 */ notifyPanelSelected(int position)496 private void notifyPanelSelected(int position) { 497 if (mDecor != null) { 498 mDecor.onPageSelected(position); 499 } 500 501 if (mPanelChangedListener != null) { 502 final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); 503 mPanelChangedListener.onPanelSelected(panelId); 504 } 505 } 506 507 private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> { 508 @Override onCreateLoader(int id, Bundle args)509 public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) { 510 return new HomeConfigLoader(mContext, mConfig); 511 } 512 513 @Override onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState)514 public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) { 515 mLoadState = LoadState.LOADED; 516 updateUiFromConfigState(configState); 517 } 518 519 @Override onLoaderReset(Loader<HomeConfig.State> loader)520 public void onLoaderReset(Loader<HomeConfig.State> loader) { 521 mLoadState = LoadState.UNLOADED; 522 } 523 } 524 525 private class PageChangeListener implements RtlViewPager.OnPageChangeListener { 526 @Override onPageSelected(int position)527 public void onPageSelected(int position) { 528 notifyPanelSelected(position); 529 530 if (mHomeBanner != null) { 531 mHomeBanner.setActive(position == mDefaultPageIndex); 532 } 533 534 // Start a UI telemetry session for the newly selected panel. 535 final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position); 536 startNewPanelTelemetrySession(newPanelId); 537 } 538 539 @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels)540 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 541 if (mDecor != null) { 542 mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); 543 } 544 545 if (mHomeBanner != null) { 546 mHomeBanner.setScrollingPages(positionOffsetPixels != 0); 547 } 548 } 549 550 @Override onPageScrollStateChanged(int state)551 public void onPageScrollStateChanged(int state) { } 552 } 553 554 /** 555 * Start UI telemetry session for the a panel. 556 * If there is currently a session open for a panel, 557 * it will be stopped before a new one is started. 558 * 559 * @param panelId of panel to start a session for 560 */ startNewPanelTelemetrySession(String panelId)561 private void startNewPanelTelemetrySession(String panelId) { 562 // Stop the current panel's session if we have one. 563 stopCurrentPanelTelemetrySession(); 564 565 mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL; 566 567 if (HomeConfig.TOP_SITES_PANEL_ID.equals(panelId) && 568 ActivityStream.isEnabled(getContext())) { 569 // Override the panel ID for Activity Stream: we're reusing the topsites panel to show 570 // Activity Stream, i.e. AS ends up havin the same panel ID. We override this for telemetry 571 // to distinguish between topsites and AS: 572 mCurrentPanelSessionSuffix = "activity_stream"; 573 } else { 574 mCurrentPanelSessionSuffix = panelId; 575 } 576 577 Telemetry.startUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix); 578 } 579 580 /** 581 * Stop the current panel telemetry session if one exists. 582 */ stopCurrentPanelTelemetrySession()583 private void stopCurrentPanelTelemetrySession() { 584 if (mCurrentPanelSession != null) { 585 Telemetry.stopUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix); 586 mCurrentPanelSession = null; 587 mCurrentPanelSessionSuffix = null; 588 } 589 } 590 } 591