Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
6 package org.mozilla.gecko.home;
8 import org.mozilla.gecko.R;
9 import org.mozilla.gecko.db.BrowserContract.HomeItems;
10 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
11 import org.mozilla.gecko.home.HomeConfig.EmptyViewConfig;
12 import org.mozilla.gecko.home.HomeConfig.ItemHandler;
13 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
14 import org.mozilla.gecko.home.HomeConfig.ViewConfig;
15 import org.mozilla.gecko.util.StringUtils;
17 import android.content.Context;
18 import android.database.Cursor;
19 import android.os.Parcel;
20 import android.os.Parcelable;
21 import android.text.TextUtils;
22 import android.util.Log;
23 import android.util.SparseArray;
24 import android.view.KeyEvent;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.FrameLayout;
29 import android.widget.ImageView;
30 import android.widget.LinearLayout;
31 import android.widget.TextView;
33 import java.lang.ref.SoftReference;
34 import java.util.EnumSet;
35 import java.util.LinkedList;
36 import java.util.Map;
37 import java.util.WeakHashMap;
39 import com.squareup.picasso.Picasso;
41 /**
42 * {@code PanelLayout} is the base class for custom layouts to be
43 * used in {@code DynamicPanel}. It provides the basic framework
44 * that enables custom layouts to request and reset datasets and
45 * create panel views. Furthermore, it automates most of the process
46 * of binding panel views with their respective datasets.
47 *
48 * {@code PanelLayout} abstracts the implemention details of how
49 * datasets are actually loaded through the {@DatasetHandler} interface.
50 * {@code DatasetHandler} provides two operations: request and reset.
51 * The results of the dataset requests done via the {@code DatasetHandler}
52 * are delivered to the {@code PanelLayout} with the {@code deliverDataset()}
53 * method.
54 *
55 * Subclasses of {@code PanelLayout} should simply use the utilities
56 * provided by {@code PanelLayout}. Namely:
57 *
58 * {@code requestDataset()} - To fetch datasets and auto-bind them to
59 * the existing panel views backed by them.
60 *
61 * {@code resetDataset()} - To release any resources associated with a
62 * previously loaded dataset.
63 *
64 * {@code createPanelView()} - To create a panel view for a ViewConfig
65 * associated with the panel.
66 *
67 * {@code disposePanelView()} - To dispose any dataset references associated
68 * with the given view.
69 *
70 * {@code PanelLayout} subclasses should always use {@code createPanelView()}
71 * to create the views dynamically created based on {@code ViewConfig}. This
72 * allows {@code PanelLayout} to auto-bind datasets with panel views.
73 * {@code PanelLayout} subclasses are free to have any type of views to arrange
74 * the panel views in different ways.
75 */
76 abstract class PanelLayout extends FrameLayout {
77 private static final String LOGTAG = "GeckoPanelLayout";
79 protected final SparseArray<ViewState> mViewStates;
80 private final PanelConfig mPanelConfig;
81 private final DatasetHandler mDatasetHandler;
82 private final OnUrlOpenListener mUrlOpenListener;
83 private final ContextMenuRegistry mContextMenuRegistry;
85 /**
86 * To be used by panel views to express that they are
87 * backed by datasets.
88 */
89 public interface DatasetBacked {
90 public void setDataset(Cursor cursor);
91 public void setFilterManager(FilterManager manager);
92 }
94 /**
95 * To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current
96 * filter for queries on the database.
97 */
98 public static class DatasetRequest implements Parcelable {
99 public enum Type implements Parcelable {
100 DATASET_LOAD,
101 FILTER_PUSH,
102 FILTER_POP;
104 @Override
105 public int describeContents() {
106 return 0;
107 }
109 @Override
110 public void writeToParcel(Parcel dest, int flags) {
111 dest.writeInt(ordinal());
112 }
114 public static final Creator<Type> CREATOR = new Creator<Type>() {
115 @Override
116 public Type createFromParcel(final Parcel source) {
117 return Type.values()[source.readInt()];
118 }
120 @Override
121 public Type[] newArray(final int size) {
122 return new Type[size];
123 }
124 };
125 }
127 private final int mViewIndex;
128 private final Type mType;
129 private final String mDatasetId;
130 private final FilterDetail mFilterDetail;
132 private DatasetRequest(Parcel in) {
133 this.mViewIndex = in.readInt();
134 this.mType = (Type) in.readParcelable(getClass().getClassLoader());
135 this.mDatasetId = in.readString();
136 this.mFilterDetail = (FilterDetail) in.readParcelable(getClass().getClassLoader());
137 }
139 public DatasetRequest(int index, String datasetId, FilterDetail filterDetail) {
140 this(index, Type.DATASET_LOAD, datasetId, filterDetail);
141 }
143 public DatasetRequest(int index, Type type, String datasetId, FilterDetail filterDetail) {
144 this.mViewIndex = index;
145 this.mType = type;
146 this.mDatasetId = datasetId;
147 this.mFilterDetail = filterDetail;
148 }
150 public int getViewIndex() {
151 return mViewIndex;
152 }
154 public Type getType() {
155 return mType;
156 }
158 public String getDatasetId() {
159 return mDatasetId;
160 }
162 public String getFilter() {
163 return (mFilterDetail != null ? mFilterDetail.filter : null);
164 }
166 public FilterDetail getFilterDetail() {
167 return mFilterDetail;
168 }
170 @Override
171 public int describeContents() {
172 return 0;
173 }
175 @Override
176 public void writeToParcel(Parcel dest, int flags) {
177 dest.writeInt(mViewIndex);
178 dest.writeParcelable(mType, 0);
179 dest.writeString(mDatasetId);
180 dest.writeParcelable(mFilterDetail, 0);
181 }
183 public String toString() {
184 return "{ index: " + mViewIndex +
185 ", type: " + mType +
186 ", dataset: " + mDatasetId +
187 ", filter: " + mFilterDetail +
188 " }";
189 }
191 public static final Creator<DatasetRequest> CREATOR = new Creator<DatasetRequest>() {
192 public DatasetRequest createFromParcel(Parcel in) {
193 return new DatasetRequest(in);
194 }
196 public DatasetRequest[] newArray(int size) {
197 return new DatasetRequest[size];
198 }
199 };
200 }
202 /**
203 * Defines the contract with the component that is responsible
204 * for handling datasets requests.
205 */
206 public interface DatasetHandler {
207 /**
208 * Requests a dataset to be fetched and auto-bound to the
209 * panel views backed by it.
210 */
211 public void requestDataset(DatasetRequest request);
213 /**
214 * Releases any resources associated with a panel view. It will
215 * do nothing if the view with the given index been created
216 * before.
217 */
218 public void resetDataset(int viewIndex);
219 }
221 public interface PanelView {
222 public void setOnItemOpenListener(OnItemOpenListener listener);
223 public void setOnKeyListener(OnKeyListener listener);
224 public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory);
225 }
227 public interface FilterManager {
228 public FilterDetail getPreviousFilter();
229 public boolean canGoBack();
230 public void goBack();
231 }
233 public interface ContextMenuRegistry {
234 public void register(View view);
235 }
237 public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler,
238 OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) {
239 super(context);
240 mViewStates = new SparseArray<ViewState>();
241 mPanelConfig = panelConfig;
242 mDatasetHandler = datasetHandler;
243 mUrlOpenListener = urlOpenListener;
244 mContextMenuRegistry = contextMenuRegistry;
245 }
247 @Override
248 public void onDetachedFromWindow() {
249 super.onDetachedFromWindow();
251 final int count = mViewStates.size();
252 for (int i = 0; i < count; i++) {
253 final ViewState viewState = mViewStates.valueAt(i);
255 final View view = viewState.getView();
256 if (view != null) {
257 maybeSetDataset(view, null);
258 }
259 }
260 mViewStates.clear();
261 }
263 /**
264 * Delivers the dataset as a {@code Cursor} to be bound to the
265 * panel view backed by it. This is used by the {@code DatasetHandler}
266 * in response to a dataset request.
267 */
268 public final void deliverDataset(DatasetRequest request, Cursor cursor) {
269 Log.d(LOGTAG, "Delivering request: " + request);
270 final ViewState viewState = mViewStates.get(request.getViewIndex());
271 if (viewState == null) {
272 return;
273 }
275 switch (request.getType()) {
276 case FILTER_PUSH:
277 viewState.pushFilter(request.getFilterDetail());
278 break;
279 case FILTER_POP:
280 viewState.popFilter();
281 break;
282 }
284 final View activeView = viewState.getActiveView();
285 if (activeView == null) {
286 throw new IllegalStateException("No active view for view state: " + viewState.getIndex());
287 }
289 final ViewConfig viewConfig = viewState.getViewConfig();
291 final View newView;
292 if (cursor == null || cursor.getCount() == 0) {
293 newView = createEmptyView(viewConfig);
294 maybeSetDataset(activeView, null);
295 } else {
296 newView = createPanelView(viewConfig);
297 maybeSetDataset(newView, cursor);
298 }
300 if (activeView != newView) {
301 replacePanelView(activeView, newView);
302 }
303 }
305 /**
306 * Releases any references to the given dataset from all
307 * existing panel views.
308 */
309 public final void releaseDataset(int viewIndex) {
310 Log.d(LOGTAG, "Releasing dataset: " + viewIndex);
311 final ViewState viewState = mViewStates.get(viewIndex);
312 if (viewState == null) {
313 return;
314 }
316 final View view = viewState.getView();
317 if (view != null) {
318 maybeSetDataset(view, null);
319 }
320 }
322 /**
323 * Requests a dataset to be loaded and bound to any existing
324 * panel view backed by it.
325 */
326 protected final void requestDataset(DatasetRequest request) {
327 Log.d(LOGTAG, "Requesting request: " + request);
328 if (mViewStates.get(request.getViewIndex()) == null) {
329 return;
330 }
332 mDatasetHandler.requestDataset(request);
333 }
335 /**
336 * Releases any resources associated with a panel view.
337 * e.g. close any associated {@code Cursor}.
338 */
339 protected final void resetDataset(int viewIndex) {
340 Log.d(LOGTAG, "Resetting view with index: " + viewIndex);
341 if (mViewStates.get(viewIndex) == null) {
342 return;
343 }
345 mDatasetHandler.resetDataset(viewIndex);
346 }
348 /**
349 * Factory method to create instance of panels from a given
350 * {@code ViewConfig}. All panel views defined in {@code PanelConfig}
351 * should be created using this method so that {@PanelLayout} can
352 * keep track of panel views and their associated datasets.
353 */
354 protected final View createPanelView(ViewConfig viewConfig) {
355 Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType());
357 ViewState viewState = mViewStates.get(viewConfig.getIndex());
358 if (viewState == null) {
359 viewState = new ViewState(viewConfig);
360 mViewStates.put(viewConfig.getIndex(), viewState);
361 }
363 View view = viewState.getView();
364 if (view == null) {
365 switch(viewConfig.getType()) {
366 case LIST:
367 view = new PanelListView(getContext(), viewConfig);
368 break;
370 case GRID:
371 view = new PanelGridView(getContext(), viewConfig);
372 break;
374 default:
375 throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName());
376 }
378 PanelView panelView = (PanelView) view;
379 panelView.setOnItemOpenListener(new PanelOnItemOpenListener(viewState));
380 panelView.setOnKeyListener(new PanelKeyListener(viewState));
381 panelView.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
382 @Override
383 public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
384 final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
385 info.url = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.URL));
386 info.title = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.TITLE));
387 return info;
388 }
389 });
391 mContextMenuRegistry.register(view);
393 if (view instanceof DatasetBacked) {
394 DatasetBacked datasetBacked = (DatasetBacked) view;
395 datasetBacked.setFilterManager(new PanelFilterManager(viewState));
397 // XXX: Disabled because of bug 1010986
398 // if (viewConfig.isRefreshEnabled()) {
399 // view = new PanelRefreshLayout(getContext(), view,
400 // mPanelConfig.getId(), viewConfig.getIndex());
401 // }
402 }
404 viewState.setView(view);
405 }
407 return view;
408 }
410 /**
411 * Dispose any dataset references associated with the
412 * given view.
413 */
414 protected final void disposePanelView(View view) {
415 Log.d(LOGTAG, "Disposing panel view");
416 final int count = mViewStates.size();
417 for (int i = 0; i < count; i++) {
418 final ViewState viewState = mViewStates.valueAt(i);
420 if (viewState.getView() == view) {
421 maybeSetDataset(view, null);
422 mViewStates.remove(viewState.getIndex());
423 break;
424 }
425 }
426 }
428 private void maybeSetDataset(View view, Cursor cursor) {
429 if (view instanceof DatasetBacked) {
430 final DatasetBacked dsb = (DatasetBacked) view;
431 dsb.setDataset(cursor);
432 }
433 }
435 private View createEmptyView(ViewConfig viewConfig) {
436 Log.d(LOGTAG, "Creating empty view: " + viewConfig.getType());
438 ViewState viewState = mViewStates.get(viewConfig.getIndex());
439 if (viewState == null) {
440 throw new IllegalStateException("No view state found for view index: " + viewConfig.getIndex());
441 }
443 View view = viewState.getEmptyView();
444 if (view == null) {
445 view = LayoutInflater.from(getContext()).inflate(R.layout.home_empty_panel, null);
447 final EmptyViewConfig emptyViewConfig = viewConfig.getEmptyViewConfig();
449 // XXX: Refactor this into a custom view (bug 985134)
450 final String text = (emptyViewConfig == null) ? null : emptyViewConfig.getText();
451 final TextView textView = (TextView) view.findViewById(R.id.home_empty_text);
452 if (TextUtils.isEmpty(text)) {
453 textView.setText(R.string.home_default_empty);
454 } else {
455 textView.setText(text);
456 }
458 final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl();
459 final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image);
461 if (TextUtils.isEmpty(imageUrl)) {
462 imageView.setImageResource(R.drawable.icon_home_empty_firefox);
463 } else {
464 Picasso.with(getContext())
465 .load(imageUrl)
466 .error(R.drawable.icon_home_empty_firefox)
467 .into(imageView);
468 }
470 viewState.setEmptyView(view);
471 }
473 return view;
474 }
476 private void replacePanelView(View currentView, View newView) {
477 final ViewGroup parent = (ViewGroup) currentView.getParent();
478 parent.addView(newView, parent.indexOfChild(currentView), currentView.getLayoutParams());
479 parent.removeView(currentView);
480 }
482 /**
483 * Must be implemented by {@code PanelLayout} subclasses to define
484 * what happens then the layout is first loaded. Should set initial
485 * UI state and request any necessary datasets.
486 */
487 public abstract void load();
489 /**
490 * Represents a 'live' instance of a panel view associated with
491 * the {@code PanelLayout}. Is responsible for tracking the history stack of filters.
492 */
493 protected class ViewState {
494 private final ViewConfig mViewConfig;
495 private SoftReference<View> mView;
496 private SoftReference<View> mEmptyView;
497 private LinkedList<FilterDetail> mFilterStack;
499 public ViewState(ViewConfig viewConfig) {
500 mViewConfig = viewConfig;
501 mView = new SoftReference<View>(null);
502 mEmptyView = new SoftReference<View>(null);
503 }
505 public ViewConfig getViewConfig() {
506 return mViewConfig;
507 }
509 public int getIndex() {
510 return mViewConfig.getIndex();
511 }
513 public View getView() {
514 return mView.get();
515 }
517 public void setView(View view) {
518 mView = new SoftReference<View>(view);
519 }
521 public View getEmptyView() {
522 return mEmptyView.get();
523 }
525 public void setEmptyView(View view) {
526 mEmptyView = new SoftReference<View>(view);
527 }
529 public View getActiveView() {
530 final View view = getView();
531 if (view != null && view.getParent() != null) {
532 return view;
533 }
535 final View emptyView = getEmptyView();
536 if (emptyView != null && emptyView.getParent() != null) {
537 return emptyView;
538 }
540 return null;
541 }
543 public String getDatasetId() {
544 return mViewConfig.getDatasetId();
545 }
547 public ItemHandler getItemHandler() {
548 return mViewConfig.getItemHandler();
549 }
551 /**
552 * Get the current filter that this view is displaying, or null if none.
553 */
554 public FilterDetail getCurrentFilter() {
555 if (mFilterStack == null) {
556 return null;
557 } else {
558 return mFilterStack.peek();
559 }
560 }
562 /**
563 * Get the previous filter that this view was displaying, or null if none.
564 */
565 public FilterDetail getPreviousFilter() {
566 if (!canPopFilter()) {
567 return null;
568 }
570 return mFilterStack.get(1);
571 }
573 /**
574 * Adds a filter to the history stack for this view.
575 */
576 public void pushFilter(FilterDetail filter) {
577 if (mFilterStack == null) {
578 mFilterStack = new LinkedList<FilterDetail>();
580 // Initialize with the initial filter.
581 mFilterStack.push(new FilterDetail(mViewConfig.getFilter(),
582 mPanelConfig.getTitle()));
583 }
585 mFilterStack.push(filter);
586 }
588 /**
589 * Remove the most recent filter from the stack.
590 *
591 * @return whether the filter was popped
592 */
593 public boolean popFilter() {
594 if (!canPopFilter()) {
595 return false;
596 }
598 mFilterStack.pop();
599 return true;
600 }
602 public boolean canPopFilter() {
603 return (mFilterStack != null && mFilterStack.size() > 1);
604 }
605 }
607 static class FilterDetail implements Parcelable {
608 final String filter;
609 final String title;
611 private FilterDetail(Parcel in) {
612 this.filter = in.readString();
613 this.title = in.readString();
614 }
616 public FilterDetail(String filter, String title) {
617 this.filter = filter;
618 this.title = title;
619 }
621 @Override
622 public int describeContents() {
623 return 0;
624 }
626 @Override
627 public void writeToParcel(Parcel dest, int flags) {
628 dest.writeString(filter);
629 dest.writeString(title);
630 }
632 public static final Creator<FilterDetail> CREATOR = new Creator<FilterDetail>() {
633 public FilterDetail createFromParcel(Parcel in) {
634 return new FilterDetail(in);
635 }
637 public FilterDetail[] newArray(int size) {
638 return new FilterDetail[size];
639 }
640 };
641 }
643 /**
644 * Pushes filter to {@code ViewState}'s stack and makes request for new filter value.
645 */
646 private void pushFilterOnView(ViewState viewState, FilterDetail filterDetail) {
647 final int index = viewState.getIndex();
648 final String datasetId = viewState.getDatasetId();
650 mDatasetHandler.requestDataset(new DatasetRequest(index,
651 DatasetRequest.Type.FILTER_PUSH,
652 datasetId,
653 filterDetail));
654 }
656 /**
657 * Pops filter from {@code ViewState}'s stack and makes request for previous filter value.
658 *
659 * @return whether the filter has changed
660 */
661 private boolean popFilterOnView(ViewState viewState) {
662 if (viewState.canPopFilter()) {
663 final int index = viewState.getIndex();
664 final String datasetId = viewState.getDatasetId();
665 final FilterDetail filterDetail = viewState.getPreviousFilter();
667 mDatasetHandler.requestDataset(new DatasetRequest(index,
668 DatasetRequest.Type.FILTER_POP,
669 datasetId,
670 filterDetail));
672 return true;
673 } else {
674 return false;
675 }
676 }
678 public interface OnItemOpenListener {
679 public void onItemOpen(String url, String title);
680 }
682 private class PanelOnItemOpenListener implements OnItemOpenListener {
683 private ViewState mViewState;
685 public PanelOnItemOpenListener(ViewState viewState) {
686 mViewState = viewState;
687 }
689 @Override
690 public void onItemOpen(String url, String title) {
691 if (StringUtils.isFilterUrl(url)) {
692 FilterDetail filterDetail = new FilterDetail(StringUtils.getFilterFromUrl(url), title);
693 pushFilterOnView(mViewState, filterDetail);
694 } else {
695 EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
696 if (mViewState.getItemHandler() == ItemHandler.INTENT) {
697 flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
698 }
700 mUrlOpenListener.onUrlOpen(url, flags);
701 }
702 }
703 }
705 private class PanelKeyListener implements View.OnKeyListener {
706 private ViewState mViewState;
708 public PanelKeyListener(ViewState viewState) {
709 mViewState = viewState;
710 }
712 @Override
713 public boolean onKey(View v, int keyCode, KeyEvent event) {
714 if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
715 return popFilterOnView(mViewState);
716 }
718 return false;
719 }
720 }
722 private class PanelFilterManager implements FilterManager {
723 private final ViewState mViewState;
725 public PanelFilterManager(ViewState viewState) {
726 mViewState = viewState;
727 }
729 @Override
730 public FilterDetail getPreviousFilter() {
731 return mViewState.getPreviousFilter();
732 }
734 @Override
735 public boolean canGoBack() {
736 return mViewState.canPopFilter();
737 }
739 @Override
740 public void goBack() {
741 popFilterOnView(mViewState);
742 }
743 }
744 }