diff --git a/mobile/android/base/home/DynamicPanel.java b/mobile/android/base/home/DynamicPanel.java index 809df3d7fd5..41a33f695db 100644 --- a/mobile/android/base/home/DynamicPanel.java +++ b/mobile/android/base/home/DynamicPanel.java @@ -10,6 +10,7 @@ import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract.HomeListItems; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.home.HomeConfig.PanelConfig; +import org.mozilla.gecko.home.PanelLayout.DatasetHandler; import android.app.Activity; import android.content.ContentResolver; @@ -18,6 +19,7 @@ import android.content.res.Configuration; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; @@ -30,24 +32,38 @@ import android.widget.ListView; import java.util.EnumSet; /** - * Fragment that displays dynamic content specified by a PanelConfig. + * Fragment that displays dynamic content specified by a {@code PanelConfig}. + * The {@code DynamicPanel} UI is built based on the given {@code LayoutType} + * and its associated list of {@code ViewConfig}. + * + * {@code DynamicPanel} manages all necessary Loaders to load panel datasets + * from their respective content providers. Each panel dataset has its own + * associated Loader. This is enforced by defining the Loader IDs based on + * their associated dataset IDs. + * + * The {@code PanelLayout} can make load and reset requests on datasets via + * the provided {@code DatasetHandler}. This way it doesn't need to know the + * details of how datasets are loaded and reset. Each time a dataset is + * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see + * {@code PanelDatasetHandler}). + * + * See {@code PanelLayout} for more details on how {@code DynamicPanel} + * receives dataset requests and delivers them back to the {@code PanelLayout}. */ public class DynamicPanel extends HomeFragment { private static final String LOGTAG = "GeckoDynamicPanel"; - // Cursor loader ID for the lists - private static final int LOADER_ID_LIST = 0; + // Dataset ID to be used by the loader + private static final String DATASET_ID = "dataset_id"; + + // The panel layout associated with this panel + private PanelLayout mLayout; // The configuration associated with this panel private PanelConfig mPanelConfig; - // XXX: Right now DynamicPanel is hard-coded to show a single ListView, - // but it should dynamically build views from mPanelConfig (bug 959777). - private HomeListAdapter mAdapter; - private ListView mList; - - // Callbacks used for the list loader - private CursorLoaderCallbacks mCursorLoaderCallbacks; + // Callbacks used for the loader + private PanelLoaderCallbacks mLoaderCallbacks; // On URL open listener private OnUrlOpenListener mUrlOpenListener; @@ -87,22 +103,30 @@ public class DynamicPanel extends HomeFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // XXX: Dynamically build views from mPanelConfig (bug 959777). - mList = new HomeListView(getActivity()); - return mList; + switch(mPanelConfig.getLayoutType()) { + case FRAME: + final PanelDatasetHandler datasetHandler = new PanelDatasetHandler(); + mLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler); + break; + + default: + throw new IllegalStateException("Unrecognized layout type in DynamicPanel"); + } + + Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType()); + + return mLayout; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - - registerForContextMenu(mList); } @Override public void onDestroyView() { super.onDestroyView(); - mList = null; + mLayout = null; } @Override @@ -122,29 +146,73 @@ public class DynamicPanel extends HomeFragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - // XXX: Dynamically set adapters from mPanelConfig (bug 959777). - mAdapter = new HomeListAdapter(getActivity(), null); - mList.setAdapter(mAdapter); - // Create callbacks before the initial loader is started. - mCursorLoaderCallbacks = new CursorLoaderCallbacks(); + mLoaderCallbacks = new PanelLoaderCallbacks(); loadIfVisible(); } @Override protected void load() { - getLoaderManager().initLoader(LOADER_ID_LIST, null, mCursorLoaderCallbacks); + Log.d(LOGTAG, "Loading layout"); + mLayout.load(); + } + + private static int generateLoaderId(String datasetId) { + return datasetId.hashCode(); } /** - * Cursor loader for the lists. + * Used by the PanelLayout to make load and reset requests to + * the holding fragment. */ - private static class HomeListLoader extends SimpleCursorLoader { - private String mProviderId; + private class PanelDatasetHandler implements DatasetHandler { + @Override + public void requestDataset(String datasetId) { + Log.d(LOGTAG, "Requesting dataset: " + datasetId); - public HomeListLoader(Context context, String providerId) { + // Ignore dataset requests while the fragment is not + // allowed to load its content. + if (!getCanLoadHint()) { + return; + } + + final Bundle bundle = new Bundle(); + bundle.putString(DATASET_ID, datasetId); + + // Ensure one loader per dataset + final int loaderId = generateLoaderId(datasetId); + getLoaderManager().restartLoader(loaderId, bundle, mLoaderCallbacks); + } + + @Override + public void resetDataset(String datasetId) { + Log.d(LOGTAG, "Resetting dataset: " + datasetId); + + final LoaderManager lm = getLoaderManager(); + final int loaderId = generateLoaderId(datasetId); + + // Release any resources associated with the dataset if + // it's currently loaded in memory. + final Loader datasetLoader = lm.getLoader(loaderId); + if (datasetLoader != null) { + datasetLoader.reset(); + } + } + } + + /** + * Cursor loader for the panel datasets. + */ + private static class PanelDatasetLoader extends SimpleCursorLoader { + private final String mDatasetId; + + public PanelDatasetLoader(Context context, String datasetId) { super(context); - mProviderId = providerId; + mDatasetId = datasetId; + } + + public String getDatasetId() { + return mDatasetId; } @Override @@ -156,51 +224,41 @@ public class DynamicPanel extends HomeFragment { appendQueryParameter(BrowserContract.PARAM_PROFILE, "default").build(); final String selection = HomeListItems.PROVIDER_ID + " = ?"; - final String[] selectionArgs = new String[] { mProviderId }; + final String[] selectionArgs = new String[] { mDatasetId }; - Log.i(LOGTAG, "Loading fake data for list provider: " + mProviderId); + Log.i(LOGTAG, "Loading fake data for list provider: " + mDatasetId); return cr.query(fakeItemsUri, null, selection, selectionArgs, null); } } - /** - * Cursor adapter for the list. - */ - private class HomeListAdapter extends CursorAdapter { - public HomeListAdapter(Context context, Cursor cursor) { - super(context, cursor, 0); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(cursor); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, parent, false); - } - } - /** * LoaderCallbacks implementation that interacts with the LoaderManager. */ - private class CursorLoaderCallbacks implements LoaderCallbacks { + private class PanelLoaderCallbacks implements LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { - return new HomeListLoader(getActivity(), mPanelConfig.getId()); + final String datasetId = args.getString(DATASET_ID); + + Log.d(LOGTAG, "Creating loader for dataset: " + datasetId); + return new PanelDatasetLoader(getActivity(), datasetId); } @Override - public void onLoadFinished(Loader loader, Cursor c) { - mAdapter.swapCursor(c); + public void onLoadFinished(Loader loader, Cursor cursor) { + final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; + + Log.d(LOGTAG, "Finished loader for dataset: " + datasetLoader.getDatasetId()); + mLayout.deliverDataset(datasetLoader.getDatasetId(), cursor); } @Override public void onLoaderReset(Loader loader) { - mAdapter.swapCursor(null); + final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; + Log.d(LOGTAG, "Resetting loader for dataset: " + datasetLoader.getDatasetId()); + if (mLayout != null) { + mLayout.releaseDataset(datasetLoader.getDatasetId()); + } } } } diff --git a/mobile/android/base/home/FramePanelLayout.java b/mobile/android/base/home/FramePanelLayout.java new file mode 100644 index 00000000000..25389c786af --- /dev/null +++ b/mobile/android/base/home/FramePanelLayout.java @@ -0,0 +1,44 @@ +/* -*- 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.home.HomeConfig.PanelConfig; +import org.mozilla.gecko.home.HomeConfig.ViewConfig; + +import android.content.Context; +import android.util.Log; +import android.view.View; + +class FramePanelLayout extends PanelLayout { + private static final String LOGTAG = "GeckoFramePanelLayout"; + + private final View mChildView; + private final ViewConfig mChildConfig; + + public FramePanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler) { + super(context, panelConfig, datasetHandler); + + // This layout can only hold one view so we simply + // take the first defined view from PanelConfig. + mChildConfig = panelConfig.getViewAt(0); + if (mChildConfig == null) { + throw new IllegalStateException("FramePanelLayout requires a view in PanelConfig"); + } + + mChildView = createPanelView(mChildConfig); + addView(mChildView); + } + + @Override + public void load() { + Log.d(LOGTAG, "Loading"); + + if (mChildView instanceof DatasetBacked) { + Log.d(LOGTAG, "Requesting child dataset: " + mChildConfig.getDatasetId()); + requestDataset(mChildConfig.getDatasetId()); + } + } +} diff --git a/mobile/android/base/home/PanelLayout.java b/mobile/android/base/home/PanelLayout.java new file mode 100644 index 00000000000..9bf9d1e4b1c --- /dev/null +++ b/mobile/android/base/home/PanelLayout.java @@ -0,0 +1,227 @@ +/* -*- 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.home.HomeConfig.PanelConfig; +import org.mozilla.gecko.home.HomeConfig.ViewConfig; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@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"; + + private final List mViewEntries; + private final DatasetHandler mDatasetHandler; + + /** + * To be used by panel views to express that they are + * backed by datasets. + */ + public interface DatasetBacked { + public void setDataset(Cursor cursor); + } + + /** + * 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(String datasetId); + + /** + * Releases any resources associated with a previously loaded + * dataset. It will do nothing if the dataset with the given ID + * hasn't been loaded before. + */ + public void resetDataset(String datasetId); + } + + public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler) { + super(context); + mViewEntries = new ArrayList(); + mDatasetHandler = datasetHandler; + } + + /** + * Delivers the dataset as a {@code Cursor} to be bound to the + * panel views backed by it. This is used by the {@code DatasetHandler} + * in response to a dataset request. + */ + public final void deliverDataset(String datasetId, Cursor cursor) { + Log.d(LOGTAG, "Delivering dataset: " + datasetId); + updateViewsWithDataset(datasetId, cursor); + } + + /** + * Releases any references to the given dataset from all + * existing panel views. + */ + public final void releaseDataset(String datasetId) { + Log.d(LOGTAG, "Resetting dataset: " + datasetId); + updateViewsWithDataset(datasetId, null); + } + + /** + * Requests a dataset to be loaded and bound to any existing + * panel view backed by it. + */ + protected final void requestDataset(String datasetId) { + Log.d(LOGTAG, "Requesting dataset: " + datasetId); + mDatasetHandler.requestDataset(datasetId); + } + + /** + * Releases any resources associated with a previously + * loaded dataset e.g. close any associated {@code Cursor}. + */ + protected final void resetDataset(String datasetId) { + mDatasetHandler.resetDataset(datasetId); + } + + /** + * 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) { + final View view; + + Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType()); + + switch(viewConfig.getType()) { + case LIST: + view = new PanelListView(getContext(), viewConfig); + break; + + default: + throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName()); + } + + final ViewEntry entry = new ViewEntry(view, viewConfig); + mViewEntries.add(entry); + + 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 = mViewEntries.size(); + for (int i = 0; i < count; i++) { + final View entryView = mViewEntries.get(i).getView(); + if (view == entryView) { + // Release any Cursor references from the view + // if it's backed by a dataset. + maybeSetDataset(entryView, null); + + // Remove the view entry from the list + mViewEntries.remove(i); + break; + } + } + } + + private void updateViewsWithDataset(String datasetId, Cursor cursor) { + final int count = mViewEntries.size(); + for (int i = 0; i < count; i++) { + final ViewEntry entry = mViewEntries.get(i); + + // Update any views associated with the given dataset ID + if (TextUtils.equals(entry.getDatasetId(), datasetId)) { + final View view = entry.getView(); + maybeSetDataset(view, cursor); + } + } + } + + private void maybeSetDataset(View view, Cursor cursor) { + if (view instanceof DatasetBacked) { + final DatasetBacked dsb = (DatasetBacked) view; + dsb.setDataset(cursor); + } + } + + /** + * 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}. + */ + private static class ViewEntry { + private final View mView; + private final ViewConfig mViewConfig; + + public ViewEntry(View view, ViewConfig viewConfig) { + mView = view; + mViewConfig = viewConfig; + } + + public View getView() { + return mView; + } + + public String getDatasetId() { + return mViewConfig.getDatasetId(); + } + } +} \ No newline at end of file diff --git a/mobile/android/base/home/PanelListRow.java b/mobile/android/base/home/PanelListRow.java new file mode 100644 index 00000000000..76b94065ca9 --- /dev/null +++ b/mobile/android/base/home/PanelListRow.java @@ -0,0 +1,60 @@ +/* -*- 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 android.util.Log; +import org.mozilla.gecko.favicons.Favicons; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.favicons.OnFaviconLoadedListener; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.widget.FaviconView; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; + +import java.lang.ref.WeakReference; + +public class PanelListRow extends TwoLineRow { + + public PanelListRow(Context context) { + this(context, null); + } + + public PanelListRow(Context context, AttributeSet attrs) { + super(context, attrs); + + // XXX: Never show icon for now. We have to figure out + // how the images will be passed through the cursor. + final View iconView = findViewById(R.id.icon); + iconView.setVisibility(View.GONE); + } + + @Override + public void updateFromCursor(Cursor cursor) { + if (cursor == null) { + return; + } + + // XXX: This will have to be updated once we come up with the + // final schema for Panel datasets (see bug 942288). + + int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE); + final String title = cursor.getString(titleIndex); + setPrimaryText(title); + + int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); + final String url = cursor.getString(urlIndex); + setSecondaryText(url); + } +} diff --git a/mobile/android/base/home/PanelListView.java b/mobile/android/base/home/PanelListView.java new file mode 100644 index 00000000000..e9a59c4780e --- /dev/null +++ b/mobile/android/base/home/PanelListView.java @@ -0,0 +1,57 @@ +/* -*- 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.home.HomeConfig.ViewConfig; +import org.mozilla.gecko.home.PanelLayout.DatasetBacked; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +public class PanelListView extends HomeListView + implements DatasetBacked { + + private static final String LOGTAG = "GeckoPanelListView"; + + private final PanelListAdapter mAdapter; + private final ViewConfig mViewConfig; + + public PanelListView(Context context, ViewConfig viewConfig) { + super(context); + mViewConfig = viewConfig; + mAdapter = new PanelListAdapter(context); + setAdapter(mAdapter); + } + + @Override + public void setDataset(Cursor cursor) { + Log.d(LOGTAG, "Setting dataset: " + mViewConfig.getDatasetId()); + mAdapter.swapCursor(cursor); + } + + private class PanelListAdapter extends CursorAdapter { + public PanelListAdapter(Context context) { + super(context, null, 0); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final PanelListRow row = (PanelListRow) view; + row.updateFromCursor(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.panel_list_row, parent, false); + } + } +} \ No newline at end of file diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 480399ba316..f1a5c596aac 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -215,6 +215,7 @@ gbjar.sources += [ 'home/BrowserSearch.java', 'home/DynamicPanel.java', 'home/FadedTextView.java', + 'home/FramePanelLayout.java', 'home/HistoryPanel.java', 'home/HomeAdapter.java', 'home/HomeBanner.java', @@ -228,6 +229,9 @@ gbjar.sources += [ 'home/LastTabsPanel.java', 'home/MostRecentPanel.java', 'home/MultiTypeCursorAdapter.java', + 'home/PanelLayout.java', + 'home/PanelListRow.java', + 'home/PanelListView.java', 'home/PanelManager.java', 'home/PinSiteDialog.java', 'home/ReadingListPanel.java', diff --git a/mobile/android/base/resources/layout/panel_list_row.xml b/mobile/android/base/resources/layout/panel_list_row.xml new file mode 100644 index 00000000000..68c5a1394f5 --- /dev/null +++ b/mobile/android/base/resources/layout/panel_list_row.xml @@ -0,0 +1,10 @@ + + + +