mobile/android/base/home/HomePager.java

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:634784dca97b
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.R;
13 import org.mozilla.gecko.Telemetry;
14 import org.mozilla.gecko.TelemetryContract;
15 import org.mozilla.gecko.animation.PropertyAnimator;
16 import org.mozilla.gecko.animation.ViewHelper;
17 import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener;
18 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
19 import org.mozilla.gecko.util.ThreadUtils;
20
21 import android.content.Context;
22 import android.graphics.drawable.Drawable;
23 import android.os.Build;
24 import android.os.Bundle;
25 import android.support.v4.app.FragmentManager;
26 import android.support.v4.app.LoaderManager;
27 import android.support.v4.app.LoaderManager.LoaderCallbacks;
28 import android.support.v4.content.Loader;
29 import android.support.v4.view.ViewPager;
30 import android.util.AttributeSet;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.ViewGroup;
34
35 public class HomePager extends ViewPager {
36 private static final int LOADER_ID_CONFIG = 0;
37
38 private final Context mContext;
39 private volatile boolean mVisible;
40 private Decor mDecor;
41 private View mTabStrip;
42 private HomeBanner mHomeBanner;
43 private int mDefaultPageIndex = -1;
44
45 private final OnAddPanelListener mAddPanelListener;
46
47 private final HomeConfig mConfig;
48 private ConfigLoaderCallbacks mConfigLoaderCallbacks;
49
50 private String mInitialPanelId;
51
52 // Cached original ViewPager background.
53 private final Drawable mOriginalBackground;
54
55 // Telemetry session for current panel.
56 private String mCurrentPanelSession;
57
58 // Current load state of HomePager.
59 private LoadState mLoadState;
60
61 // Listens for when the current panel changes.
62 private OnPanelChangeListener mPanelChangedListener;
63
64 // This is mostly used by UI tests to easily fetch
65 // specific list views at runtime.
66 static final String LIST_TAG_HISTORY = "history";
67 static final String LIST_TAG_BOOKMARKS = "bookmarks";
68 static final String LIST_TAG_READING_LIST = "reading_list";
69 static final String LIST_TAG_TOP_SITES = "top_sites";
70 static final String LIST_TAG_MOST_RECENT = "most_recent";
71 static final String LIST_TAG_LAST_TABS = "last_tabs";
72 static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
73
74 public interface OnUrlOpenListener {
75 public enum Flags {
76 ALLOW_SWITCH_TO_TAB,
77 OPEN_WITH_INTENT
78 }
79
80 public void onUrlOpen(String url, EnumSet<Flags> flags);
81 }
82
83 public interface OnNewTabsListener {
84 public void onNewTabs(String[] urls);
85 }
86
87 /**
88 * Interface for listening into ViewPager panel changes
89 */
90 public interface OnPanelChangeListener {
91 /**
92 * Called when a new panel is selected.
93 *
94 * @param panelId of the newly selected panel
95 */
96 public void onPanelSelected(String panelId);
97 }
98
99 interface OnTitleClickListener {
100 public void onTitleClicked(int index);
101 }
102
103 /**
104 * Special type of child views that could be added as pager decorations by default.
105 */
106 interface Decor {
107 public void onAddPagerView(String title);
108 public void removeAllPagerViews();
109 public void onPageSelected(int position);
110 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
111 public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener);
112 }
113
114 /**
115 * State of HomePager with respect to loading its configuration.
116 */
117 private enum LoadState {
118 UNLOADED,
119 LOADING,
120 LOADED
121 }
122
123 static final String CAN_LOAD_ARG = "canLoad";
124 static final String PANEL_CONFIG_ARG = "panelConfig";
125
126 public HomePager(Context context) {
127 this(context, null);
128 }
129
130 public HomePager(Context context, AttributeSet attrs) {
131 super(context, attrs);
132 mContext = context;
133
134 mConfig = HomeConfig.getDefault(mContext);
135 mConfigLoaderCallbacks = new ConfigLoaderCallbacks();
136
137 mAddPanelListener = new OnAddPanelListener() {
138 @Override
139 public void onAddPanel(String title) {
140 if (mDecor != null) {
141 mDecor.onAddPagerView(title);
142 }
143 }
144 };
145
146 // This is to keep all 4 panels in memory after they are
147 // selected in the pager.
148 setOffscreenPageLimit(3);
149
150 // We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft
151 // keyboard. However, if there are no focusable views (e.g. an empty reading list), the
152 // URL bar will be refocused. Therefore, we make the HomePager container focusable to
153 // ensure there is always a focusable view. This would ordinarily be done via an XML
154 // attribute, but it is not working properly.
155 setFocusableInTouchMode(true);
156
157 mOriginalBackground = getBackground();
158 setOnPageChangeListener(new PageChangeListener());
159
160 mLoadState = LoadState.UNLOADED;
161 }
162
163 @Override
164 public void addView(View child, int index, ViewGroup.LayoutParams params) {
165 if (child instanceof Decor) {
166 ((ViewPager.LayoutParams) params).isDecor = true;
167 mDecor = (Decor) child;
168 mTabStrip = child;
169
170 mDecor.setOnTitleClickListener(new OnTitleClickListener() {
171 @Override
172 public void onTitleClicked(int index) {
173 setCurrentItem(index, true);
174 }
175 });
176 } else if (child instanceof HomePagerTabStrip) {
177 mTabStrip = child;
178 }
179
180 super.addView(child, index, params);
181 }
182
183 /**
184 * Loads and initializes the pager.
185 *
186 * @param fm FragmentManager for the adapter
187 */
188 public void load(LoaderManager lm, FragmentManager fm, String panelId, PropertyAnimator animator) {
189 mLoadState = LoadState.LOADING;
190
191 mVisible = true;
192 mInitialPanelId = panelId;
193
194 // Update the home banner message each time the HomePager is loaded.
195 if (mHomeBanner != null) {
196 mHomeBanner.update();
197 }
198
199 // Only animate on post-HC devices, when a non-null animator is given
200 final boolean shouldAnimate = (animator != null && Build.VERSION.SDK_INT >= 11);
201
202 final HomeAdapter adapter = new HomeAdapter(mContext, fm);
203 adapter.setOnAddPanelListener(mAddPanelListener);
204 adapter.setCanLoadHint(!shouldAnimate);
205 setAdapter(adapter);
206
207 // Don't show the tabs strip until we have the
208 // list of panels in place.
209 mTabStrip.setVisibility(View.INVISIBLE);
210
211 // Load list of panels from configuration
212 lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks);
213
214 if (shouldAnimate) {
215 animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
216 @Override
217 public void onPropertyAnimationStart() {
218 setLayerType(View.LAYER_TYPE_HARDWARE, null);
219 }
220
221 @Override
222 public void onPropertyAnimationEnd() {
223 setLayerType(View.LAYER_TYPE_NONE, null);
224 adapter.setCanLoadHint(true);
225 }
226 });
227
228 ViewHelper.setAlpha(this, 0.0f);
229
230 animator.attach(this,
231 PropertyAnimator.Property.ALPHA,
232 1.0f);
233 }
234 Telemetry.startUISession(TelemetryContract.Session.HOME);
235 }
236
237 /**
238 * Removes all child fragments to free memory.
239 */
240 public void unload() {
241 mVisible = false;
242 setAdapter(null);
243 mLoadState = LoadState.UNLOADED;
244
245 // Stop UI Telemetry sessions.
246 stopCurrentPanelTelemetrySession();
247 Telemetry.stopUISession(TelemetryContract.Session.HOME);
248 }
249
250 /**
251 * Determines whether the pager is visible.
252 *
253 * Unlike getVisibility(), this method does not need to be called on the UI
254 * thread.
255 *
256 * @return Whether the pager and its fragments are loaded
257 */
258 public boolean isVisible() {
259 return mVisible;
260 }
261
262 @Override
263 public void setCurrentItem(int item, boolean smoothScroll) {
264 super.setCurrentItem(item, smoothScroll);
265
266 if (mDecor != null) {
267 mDecor.onPageSelected(item);
268 }
269
270 if (mHomeBanner != null) {
271 mHomeBanner.setActive(item == mDefaultPageIndex);
272 }
273 }
274
275 /**
276 * Shows a home panel. If the given panelId is null,
277 * the default panel will be shown. No action will be taken if:
278 * * HomePager has not loaded yet
279 * * Panel with the given panelId cannot be found
280 *
281 * @param panelId of the home panel to be shown.
282 */
283 public void showPanel(String panelId) {
284 if (!mVisible) {
285 return;
286 }
287
288 switch (mLoadState) {
289 case LOADING:
290 mInitialPanelId = panelId;
291 break;
292
293 case LOADED:
294 int position = mDefaultPageIndex;
295 if (panelId != null) {
296 position = ((HomeAdapter) getAdapter()).getItemPosition(panelId);
297 }
298
299 if (position > -1) {
300 setCurrentItem(position);
301 }
302 break;
303
304 default:
305 // Do nothing.
306 }
307 }
308
309 @Override
310 public boolean onInterceptTouchEvent(MotionEvent event) {
311 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
312 // Drop the soft keyboard by stealing focus from the URL bar.
313 requestFocus();
314 }
315
316 return super.onInterceptTouchEvent(event);
317 }
318
319 public void setBanner(HomeBanner banner) {
320 mHomeBanner = banner;
321 }
322
323 @Override
324 public boolean dispatchTouchEvent(MotionEvent event) {
325 if (mHomeBanner != null) {
326 mHomeBanner.handleHomeTouch(event);
327 }
328
329 return super.dispatchTouchEvent(event);
330 }
331
332 public void onToolbarFocusChange(boolean hasFocus) {
333 if (mHomeBanner == null) {
334 return;
335 }
336
337 // We should only make the banner active if the toolbar is not focused and we are on the default page
338 final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex;
339 mHomeBanner.setActive(active);
340 }
341
342 private void updateUiFromConfigState(HomeConfig.State configState) {
343 // We only care about the adapter if HomePager is currently
344 // loaded, which means it's visible in the activity.
345 if (!mVisible) {
346 return;
347 }
348
349 if (mDecor != null) {
350 mDecor.removeAllPagerViews();
351 }
352
353 final HomeAdapter adapter = (HomeAdapter) getAdapter();
354
355 // Disable any fragment loading until we have the initial
356 // panel selection done. Store previous value to restore
357 // it if necessary once the UI is fully updated.
358 final boolean canLoadHint = adapter.getCanLoadHint();
359 adapter.setCanLoadHint(false);
360
361 // Destroy any existing panels currently loaded
362 // in the pager.
363 setAdapter(null);
364
365 // Only keep enabled panels.
366 final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
367
368 for (PanelConfig panelConfig : configState) {
369 if (!panelConfig.isDisabled()) {
370 enabledPanels.add(panelConfig);
371 }
372 }
373
374 // Update the adapter with the new panel configs
375 adapter.update(enabledPanels);
376
377 final int count = enabledPanels.size();
378 if (count == 0) {
379 // Set firefox watermark as background.
380 setBackgroundResource(R.drawable.home_pager_empty_state);
381 // Hide the tab strip as there are no panels.
382 mTabStrip.setVisibility(View.INVISIBLE);
383 } else {
384 mTabStrip.setVisibility(View.VISIBLE);
385 // Restore original background.
386 setBackgroundDrawable(mOriginalBackground);
387 }
388
389 // Re-install the adapter with the final state
390 // in the pager.
391 setAdapter(adapter);
392
393 if (count == 0) {
394 mDefaultPageIndex = -1;
395
396 // Hide the banner if there are no enabled panels.
397 if (mHomeBanner != null) {
398 mHomeBanner.setActive(false);
399 }
400 } else {
401 for (int i = 0; i < count; i++) {
402 if (enabledPanels.get(i).isDefault()) {
403 mDefaultPageIndex = i;
404 break;
405 }
406 }
407
408 // Use the default panel if the initial panel wasn't explicitly set by the
409 // load() caller, or if the initial panel is not found in the adapter.
410 final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
411 if (itemPosition > -1) {
412 setCurrentItem(itemPosition, false);
413 mInitialPanelId = null;
414 } else {
415 setCurrentItem(mDefaultPageIndex, false);
416 }
417 }
418
419 // If the load hint was originally true, this means the pager
420 // is not animating and it's fine to restore the load hint back.
421 if (canLoadHint) {
422 // The selection is updated asynchronously so we need to post to
423 // UI thread to give the pager time to commit the new page selection
424 // internally and load the right initial panel.
425 ThreadUtils.getUiHandler().post(new Runnable() {
426 @Override
427 public void run() {
428 adapter.setCanLoadHint(true);
429 }
430 });
431 }
432 }
433
434 public void setOnPanelChangeListener(OnPanelChangeListener listener) {
435 mPanelChangedListener = listener;
436 }
437
438 /**
439 * Notify listeners of newly selected panel.
440 *
441 * @param position of the newly selected panel
442 */
443 private void notifyPanelSelected(int position) {
444 if (mDecor != null) {
445 mDecor.onPageSelected(position);
446 }
447
448 if (mPanelChangedListener != null) {
449 final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
450 mPanelChangedListener.onPanelSelected(panelId);
451 }
452 }
453
454 private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
455 @Override
456 public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
457 return new HomeConfigLoader(mContext, mConfig);
458 }
459
460 @Override
461 public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
462 mLoadState = LoadState.LOADED;
463 updateUiFromConfigState(configState);
464 }
465
466 @Override
467 public void onLoaderReset(Loader<HomeConfig.State> loader) {
468 mLoadState = LoadState.UNLOADED;
469 }
470 }
471
472 private class PageChangeListener implements ViewPager.OnPageChangeListener {
473 @Override
474 public void onPageSelected(int position) {
475 notifyPanelSelected(position);
476
477 if (mHomeBanner != null) {
478 mHomeBanner.setActive(position == mDefaultPageIndex);
479 }
480
481 // Start a UI telemetry session for the newly selected panel.
482 final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
483 startNewPanelTelemetrySession(newPanelId);
484 }
485
486 @Override
487 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
488 if (mDecor != null) {
489 mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
490 }
491
492 if (mHomeBanner != null) {
493 mHomeBanner.setScrollingPages(positionOffsetPixels != 0);
494 }
495 }
496
497 @Override
498 public void onPageScrollStateChanged(int state) { }
499 }
500
501 /**
502 * Start UI telemetry session for the a panel.
503 * If there is currently a session open for a panel,
504 * it will be stopped before a new one is started.
505 *
506 * @param panelId of panel to start a session for
507 */
508 private void startNewPanelTelemetrySession(String panelId) {
509 // Stop the current panel's session if we have one.
510 stopCurrentPanelTelemetrySession();
511
512 mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL + panelId;
513 Telemetry.startUISession(mCurrentPanelSession);
514 }
515
516 /**
517 * Stop the current panel telemetry session if one exists.
518 */
519 private void stopCurrentPanelTelemetrySession() {
520 if (mCurrentPanelSession != null) {
521 Telemetry.stopUISession(mCurrentPanelSession);
522 mCurrentPanelSession = null;
523 }
524 }
525 }

mercurial