Bug 959777 - Dynamically build the UI of DynamicPanel from a PanelConfig (r=margaret)

This commit is contained in:
Lucas Rocha 2014-01-27 13:29:55 -08:00
parent a77e140937
commit 1042c6efea
7 changed files with 514 additions and 54 deletions

View File

@ -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<Cursor> {
private class PanelLoaderCallbacks implements LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> 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<Cursor> loader, Cursor c) {
mAdapter.swapCursor(c);
public void onLoadFinished(Loader<Cursor> 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<Cursor> 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());
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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<ViewEntry> 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<ViewEntry>();
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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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',

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<org.mozilla.gecko.home.PanelListRow xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="@dimen/page_row_height"
android:paddingLeft="10dip"
android:minHeight="@dimen/page_row_height"/>