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