|
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 } |