/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.home; import org.mozilla.gecko.R; import org.mozilla.gecko.db.BrowserContract.HomeItems; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.home.HomeConfig.EmptyViewConfig; import org.mozilla.gecko.home.HomeConfig.ItemHandler; import org.mozilla.gecko.home.HomeConfig.PanelConfig; import org.mozilla.gecko.home.HomeConfig.ViewConfig; import org.mozilla.gecko.util.StringUtils; import android.content.Context; import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import java.lang.ref.SoftReference; import java.util.EnumSet; import java.util.LinkedList; import java.util.Map; import java.util.WeakHashMap; import com.squareup.picasso.Picasso; /** * {@code PanelLayout} is the base class for custom layouts to be * used in {@code DynamicPanel}. It provides the basic framework * that enables custom layouts to request and reset datasets and * create panel views. Furthermore, it automates most of the process * of binding panel views with their respective datasets. * * {@code PanelLayout} abstracts the implemention details of how * datasets are actually loaded through the {@DatasetHandler} interface. * {@code DatasetHandler} provides two operations: request and reset. * The results of the dataset requests done via the {@code DatasetHandler} * are delivered to the {@code PanelLayout} with the {@code deliverDataset()} * method. * * Subclasses of {@code PanelLayout} should simply use the utilities * provided by {@code PanelLayout}. Namely: * * {@code requestDataset()} - To fetch datasets and auto-bind them to * the existing panel views backed by them. * * {@code resetDataset()} - To release any resources associated with a * previously loaded dataset. * * {@code createPanelView()} - To create a panel view for a ViewConfig * associated with the panel. * * {@code disposePanelView()} - To dispose any dataset references associated * with the given view. * * {@code PanelLayout} subclasses should always use {@code createPanelView()} * to create the views dynamically created based on {@code ViewConfig}. This * allows {@code PanelLayout} to auto-bind datasets with panel views. * {@code PanelLayout} subclasses are free to have any type of views to arrange * the panel views in different ways. */ abstract class PanelLayout extends FrameLayout { private static final String LOGTAG = "GeckoPanelLayout"; protected final SparseArray mViewStates; private final PanelConfig mPanelConfig; private final DatasetHandler mDatasetHandler; private final OnUrlOpenListener mUrlOpenListener; private final ContextMenuRegistry mContextMenuRegistry; /** * To be used by panel views to express that they are * backed by datasets. */ public interface DatasetBacked { public void setDataset(Cursor cursor); public void setFilterManager(FilterManager manager); } /** * To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current * filter for queries on the database. */ public static class DatasetRequest implements Parcelable { public enum Type implements Parcelable { DATASET_LOAD, FILTER_PUSH, FILTER_POP; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(ordinal()); } public static final Creator CREATOR = new Creator() { @Override public Type createFromParcel(final Parcel source) { return Type.values()[source.readInt()]; } @Override public Type[] newArray(final int size) { return new Type[size]; } }; } private final int mViewIndex; private final Type mType; private final String mDatasetId; private final FilterDetail mFilterDetail; private DatasetRequest(Parcel in) { this.mViewIndex = in.readInt(); this.mType = (Type) in.readParcelable(getClass().getClassLoader()); this.mDatasetId = in.readString(); this.mFilterDetail = (FilterDetail) in.readParcelable(getClass().getClassLoader()); } public DatasetRequest(int index, String datasetId, FilterDetail filterDetail) { this(index, Type.DATASET_LOAD, datasetId, filterDetail); } public DatasetRequest(int index, Type type, String datasetId, FilterDetail filterDetail) { this.mViewIndex = index; this.mType = type; this.mDatasetId = datasetId; this.mFilterDetail = filterDetail; } public int getViewIndex() { return mViewIndex; } public Type getType() { return mType; } public String getDatasetId() { return mDatasetId; } public String getFilter() { return (mFilterDetail != null ? mFilterDetail.filter : null); } public FilterDetail getFilterDetail() { return mFilterDetail; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mViewIndex); dest.writeParcelable(mType, 0); dest.writeString(mDatasetId); dest.writeParcelable(mFilterDetail, 0); } public String toString() { return "{ index: " + mViewIndex + ", type: " + mType + ", dataset: " + mDatasetId + ", filter: " + mFilterDetail + " }"; } public static final Creator CREATOR = new Creator() { @Override public DatasetRequest createFromParcel(Parcel in) { return new DatasetRequest(in); } @Override public DatasetRequest[] newArray(int size) { return new DatasetRequest[size]; } }; } /** * Defines the contract with the component that is responsible * for handling datasets requests. */ public interface DatasetHandler { /** * Requests a dataset to be fetched and auto-bound to the * panel views backed by it. */ public void requestDataset(DatasetRequest request); /** * Releases any resources associated with a panel view. It will * do nothing if the view with the given index been created * before. */ public void resetDataset(int viewIndex); } public interface PanelView { public void setOnItemOpenListener(OnItemOpenListener listener); public void setOnKeyListener(OnKeyListener listener); public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory); } public interface FilterManager { public FilterDetail getPreviousFilter(); public boolean canGoBack(); public void goBack(); } public interface ContextMenuRegistry { public void register(View view); } public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler, OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) { super(context); mViewStates = new SparseArray(); mPanelConfig = panelConfig; mDatasetHandler = datasetHandler; mUrlOpenListener = urlOpenListener; mContextMenuRegistry = contextMenuRegistry; } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); final int count = mViewStates.size(); for (int i = 0; i < count; i++) { final ViewState viewState = mViewStates.valueAt(i); final View view = viewState.getView(); if (view != null) { maybeSetDataset(view, null); } } mViewStates.clear(); } /** * Delivers the dataset as a {@code Cursor} to be bound to the * panel view backed by it. This is used by the {@code DatasetHandler} * in response to a dataset request. */ public final void deliverDataset(DatasetRequest request, Cursor cursor) { Log.d(LOGTAG, "Delivering request: " + request); final ViewState viewState = mViewStates.get(request.getViewIndex()); if (viewState == null) { return; } switch (request.getType()) { case FILTER_PUSH: viewState.pushFilter(request.getFilterDetail()); break; case FILTER_POP: viewState.popFilter(); break; } final View activeView = viewState.getActiveView(); if (activeView == null) { throw new IllegalStateException("No active view for view state: " + viewState.getIndex()); } final ViewConfig viewConfig = viewState.getViewConfig(); final View newView; if (cursor == null || cursor.getCount() == 0) { newView = createEmptyView(viewConfig); maybeSetDataset(activeView, null); } else { newView = createPanelView(viewConfig); maybeSetDataset(newView, cursor); } if (activeView != newView) { replacePanelView(activeView, newView); } } /** * Releases any references to the given dataset from all * existing panel views. */ public final void releaseDataset(int viewIndex) { Log.d(LOGTAG, "Releasing dataset: " + viewIndex); final ViewState viewState = mViewStates.get(viewIndex); if (viewState == null) { return; } final View view = viewState.getView(); if (view != null) { maybeSetDataset(view, null); } } /** * Requests a dataset to be loaded and bound to any existing * panel view backed by it. */ protected final void requestDataset(DatasetRequest request) { Log.d(LOGTAG, "Requesting request: " + request); if (mViewStates.get(request.getViewIndex()) == null) { return; } mDatasetHandler.requestDataset(request); } /** * Releases any resources associated with a panel view. * e.g. close any associated {@code Cursor}. */ protected final void resetDataset(int viewIndex) { Log.d(LOGTAG, "Resetting view with index: " + viewIndex); if (mViewStates.get(viewIndex) == null) { return; } mDatasetHandler.resetDataset(viewIndex); } /** * Factory method to create instance of panels from a given * {@code ViewConfig}. All panel views defined in {@code PanelConfig} * should be created using this method so that {@PanelLayout} can * keep track of panel views and their associated datasets. */ protected final View createPanelView(ViewConfig viewConfig) { Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType()); ViewState viewState = mViewStates.get(viewConfig.getIndex()); if (viewState == null) { viewState = new ViewState(viewConfig); mViewStates.put(viewConfig.getIndex(), viewState); } View view = viewState.getView(); if (view == null) { switch (viewConfig.getType()) { case LIST: view = new PanelListView(getContext(), viewConfig); break; case GRID: view = new PanelRecyclerView(getContext(), viewConfig); break; default: throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName()); } PanelView panelView = (PanelView) view; panelView.setOnItemOpenListener(new PanelOnItemOpenListener(viewState)); panelView.setOnKeyListener(new PanelKeyListener(viewState)); panelView.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() { @Override public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) { final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id); info.url = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.URL)); info.title = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.TITLE)); return info; } }); mContextMenuRegistry.register(view); if (view instanceof DatasetBacked) { DatasetBacked datasetBacked = (DatasetBacked) view; datasetBacked.setFilterManager(new PanelFilterManager(viewState)); if (viewConfig.isRefreshEnabled()) { view = new PanelRefreshLayout(getContext(), view, mPanelConfig.getId(), viewConfig.getIndex()); } } viewState.setView(view); } return view; } /** * Dispose any dataset references associated with the * given view. */ protected final void disposePanelView(View view) { Log.d(LOGTAG, "Disposing panel view"); final int count = mViewStates.size(); for (int i = 0; i < count; i++) { final ViewState viewState = mViewStates.valueAt(i); if (viewState.getView() == view) { maybeSetDataset(view, null); mViewStates.remove(viewState.getIndex()); break; } } } private void maybeSetDataset(View view, Cursor cursor) { if (view instanceof DatasetBacked) { final DatasetBacked dsb = (DatasetBacked) view; dsb.setDataset(cursor); } } private View createEmptyView(ViewConfig viewConfig) { Log.d(LOGTAG, "Creating empty view: " + viewConfig.getType()); ViewState viewState = mViewStates.get(viewConfig.getIndex()); if (viewState == null) { throw new IllegalStateException("No view state found for view index: " + viewConfig.getIndex()); } View view = viewState.getEmptyView(); if (view == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.home_empty_panel, null); final EmptyViewConfig emptyViewConfig = viewConfig.getEmptyViewConfig(); // XXX: Refactor this into a custom view (bug 985134) final String text = (emptyViewConfig == null) ? null : emptyViewConfig.getText(); final TextView textView = (TextView) view.findViewById(R.id.home_empty_text); if (TextUtils.isEmpty(text)) { textView.setText(R.string.home_default_empty); } else { textView.setText(text); } final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl(); final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image); if (TextUtils.isEmpty(imageUrl)) { imageView.setImageResource(R.drawable.icon_home_empty_firefox); } else { ImageLoader.with(getContext()) .load(imageUrl) .error(R.drawable.icon_home_empty_firefox) .into(imageView); } viewState.setEmptyView(view); } return view; } private void replacePanelView(View currentView, View newView) { final ViewGroup parent = (ViewGroup) currentView.getParent(); parent.addView(newView, parent.indexOfChild(currentView), currentView.getLayoutParams()); parent.removeView(currentView); } /** * Must be implemented by {@code PanelLayout} subclasses to define * what happens then the layout is first loaded. Should set initial * UI state and request any necessary datasets. */ public abstract void load(); /** * Represents a 'live' instance of a panel view associated with * the {@code PanelLayout}. Is responsible for tracking the history stack of filters. */ protected class ViewState { private final ViewConfig mViewConfig; private SoftReference mView; private SoftReference mEmptyView; private LinkedList mFilterStack; public ViewState(ViewConfig viewConfig) { mViewConfig = viewConfig; mView = new SoftReference(null); mEmptyView = new SoftReference(null); } public ViewConfig getViewConfig() { return mViewConfig; } public int getIndex() { return mViewConfig.getIndex(); } public View getView() { return mView.get(); } public void setView(View view) { mView = new SoftReference(view); } public View getEmptyView() { return mEmptyView.get(); } public void setEmptyView(View view) { mEmptyView = new SoftReference(view); } public View getActiveView() { final View view = getView(); if (view != null && view.getParent() != null) { return view; } final View emptyView = getEmptyView(); if (emptyView != null && emptyView.getParent() != null) { return emptyView; } return null; } public String getDatasetId() { return mViewConfig.getDatasetId(); } public ItemHandler getItemHandler() { return mViewConfig.getItemHandler(); } /** * Get the current filter that this view is displaying, or null if none. */ public FilterDetail getCurrentFilter() { if (mFilterStack == null) { return null; } else { return mFilterStack.peek(); } } /** * Get the previous filter that this view was displaying, or null if none. */ public FilterDetail getPreviousFilter() { if (!canPopFilter()) { return null; } return mFilterStack.get(1); } /** * Adds a filter to the history stack for this view. */ public void pushFilter(FilterDetail filter) { if (mFilterStack == null) { mFilterStack = new LinkedList(); // Initialize with the initial filter. mFilterStack.push(new FilterDetail(mViewConfig.getFilter(), mPanelConfig.getTitle())); } mFilterStack.push(filter); } /** * Remove the most recent filter from the stack. * * @return whether the filter was popped */ public boolean popFilter() { if (!canPopFilter()) { return false; } mFilterStack.pop(); return true; } public boolean canPopFilter() { return (mFilterStack != null && mFilterStack.size() > 1); } } static class FilterDetail implements Parcelable { final String filter; final String title; private FilterDetail(Parcel in) { this.filter = in.readString(); this.title = in.readString(); } public FilterDetail(String filter, String title) { this.filter = filter; this.title = title; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(filter); dest.writeString(title); } public static final Creator CREATOR = new Creator() { @Override public FilterDetail createFromParcel(Parcel in) { return new FilterDetail(in); } @Override public FilterDetail[] newArray(int size) { return new FilterDetail[size]; } }; } /** * Pushes filter to {@code ViewState}'s stack and makes request for new filter value. */ private void pushFilterOnView(ViewState viewState, FilterDetail filterDetail) { final int index = viewState.getIndex(); final String datasetId = viewState.getDatasetId(); mDatasetHandler.requestDataset(new DatasetRequest(index, DatasetRequest.Type.FILTER_PUSH, datasetId, filterDetail)); } /** * Pops filter from {@code ViewState}'s stack and makes request for previous filter value. * * @return whether the filter has changed */ private boolean popFilterOnView(ViewState viewState) { if (viewState.canPopFilter()) { final int index = viewState.getIndex(); final String datasetId = viewState.getDatasetId(); final FilterDetail filterDetail = viewState.getPreviousFilter(); mDatasetHandler.requestDataset(new DatasetRequest(index, DatasetRequest.Type.FILTER_POP, datasetId, filterDetail)); return true; } else { return false; } } public interface OnItemOpenListener { public void onItemOpen(String url, String title); } private class PanelOnItemOpenListener implements OnItemOpenListener { private final ViewState mViewState; public PanelOnItemOpenListener(ViewState viewState) { mViewState = viewState; } @Override public void onItemOpen(String url, String title) { if (StringUtils.isFilterUrl(url)) { FilterDetail filterDetail = new FilterDetail(StringUtils.getFilterFromUrl(url), title); pushFilterOnView(mViewState, filterDetail); } else { EnumSet flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class); if (mViewState.getItemHandler() == ItemHandler.INTENT) { flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT); } mUrlOpenListener.onUrlOpen(url, flags); } } } private class PanelKeyListener implements View.OnKeyListener { private final ViewState mViewState; public PanelKeyListener(ViewState viewState) { mViewState = viewState; } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { return popFilterOnView(mViewState); } return false; } } private class PanelFilterManager implements FilterManager { private final ViewState mViewState; public PanelFilterManager(ViewState viewState) { mViewState = viewState; } @Override public FilterDetail getPreviousFilter() { return mViewState.getPreviousFilter(); } @Override public boolean canGoBack() { return mViewState.canPopFilter(); } @Override public void goBack() { popFilterOnView(mViewState); } } }