michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.db.BrowserContract; michael@0: import org.mozilla.gecko.db.BrowserContract.HomeItems; michael@0: import org.mozilla.gecko.db.DBUtils; michael@0: import org.mozilla.gecko.db.HomeProvider; michael@0: import org.mozilla.gecko.home.HomeConfig.PanelConfig; michael@0: import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; michael@0: import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry; michael@0: import org.mozilla.gecko.home.PanelLayout.DatasetHandler; michael@0: import org.mozilla.gecko.home.PanelLayout.DatasetRequest; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: michael@0: import android.app.Activity; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.res.Configuration; michael@0: import android.database.Cursor; michael@0: import android.net.Uri; michael@0: import android.os.Bundle; michael@0: import android.support.v4.app.LoaderManager; michael@0: import android.support.v4.app.LoaderManager.LoaderCallbacks; michael@0: import android.support.v4.content.Loader; michael@0: import android.util.Log; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.widget.FrameLayout; michael@0: michael@0: /** michael@0: * Fragment that displays dynamic content specified by a {@code PanelConfig}. michael@0: * The {@code DynamicPanel} UI is built based on the given {@code LayoutType} michael@0: * and its associated list of {@code ViewConfig}. michael@0: * michael@0: * {@code DynamicPanel} manages all necessary Loaders to load panel datasets michael@0: * from their respective content providers. Each panel dataset has its own michael@0: * associated Loader. This is enforced by defining the Loader IDs based on michael@0: * their associated dataset IDs. michael@0: * michael@0: * The {@code PanelLayout} can make load and reset requests on datasets via michael@0: * the provided {@code DatasetHandler}. This way it doesn't need to know the michael@0: * details of how datasets are loaded and reset. Each time a dataset is michael@0: * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see michael@0: * {@code PanelDatasetHandler}). michael@0: * michael@0: * See {@code PanelLayout} for more details on how {@code DynamicPanel} michael@0: * receives dataset requests and delivers them back to the {@code PanelLayout}. michael@0: */ michael@0: public class DynamicPanel extends HomeFragment { michael@0: private static final String LOGTAG = "GeckoDynamicPanel"; michael@0: michael@0: // Dataset ID to be used by the loader michael@0: private static final String DATASET_REQUEST = "dataset_request"; michael@0: michael@0: // The main view for this fragment. This contains the PanelLayout and PanelAuthLayout. michael@0: private FrameLayout mView; michael@0: michael@0: // The panel layout associated with this panel michael@0: private PanelLayout mPanelLayout; michael@0: michael@0: // The layout used to show authentication UI for this panel michael@0: private PanelAuthLayout mPanelAuthLayout; michael@0: michael@0: // Cache used to keep track of whether or not the user has been authenticated. michael@0: private PanelAuthCache mPanelAuthCache; michael@0: michael@0: // Hold a reference to the UiAsyncTask we use to check the state of the michael@0: // PanelAuthCache, so that we can cancel it if necessary. michael@0: private UiAsyncTask mAuthStateTask; michael@0: michael@0: // The configuration associated with this panel michael@0: private PanelConfig mPanelConfig; michael@0: michael@0: // Callbacks used for the loader michael@0: private PanelLoaderCallbacks mLoaderCallbacks; michael@0: michael@0: // On URL open listener michael@0: private OnUrlOpenListener mUrlOpenListener; michael@0: michael@0: // The current UI mode in the fragment michael@0: private UIMode mUIMode; michael@0: michael@0: /* michael@0: * Different UI modes to display depending on the authentication state. michael@0: * michael@0: * PANEL: Layout to display panel data. michael@0: * AUTH: Authentication UI. michael@0: */ michael@0: private enum UIMode { michael@0: PANEL, michael@0: AUTH michael@0: } michael@0: michael@0: @Override michael@0: public void onAttach(Activity activity) { michael@0: super.onAttach(activity); michael@0: michael@0: try { michael@0: mUrlOpenListener = (OnUrlOpenListener) activity; michael@0: } catch (ClassCastException e) { michael@0: throw new ClassCastException(activity.toString() michael@0: + " must implement HomePager.OnUrlOpenListener"); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onDetach() { michael@0: super.onDetach(); michael@0: michael@0: mUrlOpenListener = null; michael@0: } michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: super.onCreate(savedInstanceState); michael@0: michael@0: final Bundle args = getArguments(); michael@0: if (args != null) { michael@0: mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG); michael@0: } michael@0: michael@0: if (mPanelConfig == null) { michael@0: throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig"); michael@0: } michael@0: michael@0: mPanelAuthCache = new PanelAuthCache(getActivity()); michael@0: } michael@0: michael@0: @Override michael@0: public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { michael@0: mView = new FrameLayout(getActivity()); michael@0: return mView; michael@0: } michael@0: michael@0: @Override michael@0: public void onViewCreated(View view, Bundle savedInstanceState) { michael@0: super.onViewCreated(view, savedInstanceState); michael@0: michael@0: // Restore whatever the UI mode the fragment had before michael@0: // a device rotation. michael@0: if (mUIMode != null) { michael@0: setUIMode(mUIMode); michael@0: } michael@0: michael@0: mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener()); michael@0: } michael@0: michael@0: @Override michael@0: public void onDestroyView() { michael@0: super.onDestroyView(); michael@0: mView = null; michael@0: mPanelLayout = null; michael@0: mPanelAuthLayout = null; michael@0: michael@0: mPanelAuthCache.setOnChangeListener(null); michael@0: michael@0: if (mAuthStateTask != null) { michael@0: mAuthStateTask.cancel(true); michael@0: mAuthStateTask = null; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onConfigurationChanged(Configuration newConfig) { michael@0: super.onConfigurationChanged(newConfig); michael@0: michael@0: // Detach and reattach the fragment as the layout changes. michael@0: if (isVisible()) { michael@0: getFragmentManager().beginTransaction() michael@0: .detach(this) michael@0: .attach(this) michael@0: .commitAllowingStateLoss(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onActivityCreated(Bundle savedInstanceState) { michael@0: super.onActivityCreated(savedInstanceState); michael@0: michael@0: // Create callbacks before the initial loader is started. michael@0: mLoaderCallbacks = new PanelLoaderCallbacks(); michael@0: loadIfVisible(); michael@0: } michael@0: michael@0: @Override michael@0: protected void load() { michael@0: Log.d(LOGTAG, "Loading layout"); michael@0: michael@0: if (requiresAuth()) { michael@0: mAuthStateTask = new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: public synchronized Boolean doInBackground(Void... params) { michael@0: return mPanelAuthCache.isAuthenticated(mPanelConfig.getId()); michael@0: } michael@0: michael@0: @Override michael@0: public void onPostExecute(Boolean isAuthenticated) { michael@0: mAuthStateTask = null; michael@0: setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); michael@0: } michael@0: }; michael@0: mAuthStateTask.execute(); michael@0: } else { michael@0: setUIMode(UIMode.PANEL); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @return true if this panel requires authentication. michael@0: */ michael@0: private boolean requiresAuth() { michael@0: return mPanelConfig.getAuthConfig() != null; michael@0: } michael@0: michael@0: /** michael@0: * Lazily creates layout for panel data. michael@0: */ michael@0: private void createPanelLayout() { michael@0: final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() { michael@0: @Override michael@0: public void register(View view) { michael@0: registerForContextMenu(view); michael@0: } michael@0: }; michael@0: michael@0: switch(mPanelConfig.getLayoutType()) { michael@0: case FRAME: michael@0: final PanelDatasetHandler datasetHandler = new PanelDatasetHandler(); michael@0: mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler, michael@0: mUrlOpenListener, contextMenuRegistry); michael@0: break; michael@0: michael@0: default: michael@0: throw new IllegalStateException("Unrecognized layout type in DynamicPanel"); michael@0: } michael@0: michael@0: Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType()); michael@0: mView.addView(mPanelLayout); michael@0: } michael@0: michael@0: /** michael@0: * Lazily creates layout for authentication UI. michael@0: */ michael@0: private void createPanelAuthLayout() { michael@0: mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig); michael@0: mView.addView(mPanelAuthLayout, 0); michael@0: } michael@0: michael@0: private void setUIMode(UIMode mode) { michael@0: switch(mode) { michael@0: case PANEL: michael@0: if (mPanelAuthLayout != null) { michael@0: mPanelAuthLayout.setVisibility(View.GONE); michael@0: } michael@0: if (mPanelLayout == null) { michael@0: createPanelLayout(); michael@0: } michael@0: mPanelLayout.setVisibility(View.VISIBLE); michael@0: michael@0: // Only trigger a reload if the UI mode has changed michael@0: // (e.g. auth cache changes) and the fragment is allowed michael@0: // to load its contents. Any loaders associated with the michael@0: // panel layout will be automatically re-bound after a michael@0: // device rotation, no need to explicitly load it again. michael@0: if (mUIMode != mode && canLoad()) { michael@0: mPanelLayout.load(); michael@0: } michael@0: break; michael@0: michael@0: case AUTH: michael@0: if (mPanelLayout != null) { michael@0: mPanelLayout.setVisibility(View.GONE); michael@0: } michael@0: if (mPanelAuthLayout == null) { michael@0: createPanelAuthLayout(); michael@0: } michael@0: mPanelAuthLayout.setVisibility(View.VISIBLE); michael@0: break; michael@0: michael@0: default: michael@0: throw new IllegalStateException("Unrecognized UIMode in DynamicPanel"); michael@0: } michael@0: michael@0: mUIMode = mode; michael@0: } michael@0: michael@0: /** michael@0: * Used by the PanelLayout to make load and reset requests to michael@0: * the holding fragment. michael@0: */ michael@0: private class PanelDatasetHandler implements DatasetHandler { michael@0: @Override michael@0: public void requestDataset(DatasetRequest request) { michael@0: Log.d(LOGTAG, "Requesting request: " + request); michael@0: michael@0: // Ignore dataset requests while the fragment is not michael@0: // allowed to load its content. michael@0: if (!getCanLoadHint()) { michael@0: return; michael@0: } michael@0: michael@0: final Bundle bundle = new Bundle(); michael@0: bundle.putParcelable(DATASET_REQUEST, request); michael@0: michael@0: getLoaderManager().restartLoader(request.getViewIndex(), michael@0: bundle, mLoaderCallbacks); michael@0: } michael@0: michael@0: @Override michael@0: public void resetDataset(int viewIndex) { michael@0: Log.d(LOGTAG, "Resetting dataset: " + viewIndex); michael@0: michael@0: final LoaderManager lm = getLoaderManager(); michael@0: michael@0: // Release any resources associated with the dataset if michael@0: // it's currently loaded in memory. michael@0: final Loader datasetLoader = lm.getLoader(viewIndex); michael@0: if (datasetLoader != null) { michael@0: datasetLoader.reset(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Cursor loader for the panel datasets. michael@0: */ michael@0: private static class PanelDatasetLoader extends SimpleCursorLoader { michael@0: private DatasetRequest mRequest; michael@0: michael@0: public PanelDatasetLoader(Context context, DatasetRequest request) { michael@0: super(context); michael@0: mRequest = request; michael@0: } michael@0: michael@0: public DatasetRequest getRequest() { michael@0: return mRequest; michael@0: } michael@0: michael@0: @Override michael@0: public void onContentChanged() { michael@0: // Ensure the refresh request doesn't affect the view's filter michael@0: // stack (i.e. use DATASET_LOAD type) but keep the current michael@0: // dataset ID and filter. michael@0: final DatasetRequest newRequest = michael@0: new DatasetRequest(mRequest.getViewIndex(), michael@0: DatasetRequest.Type.DATASET_LOAD, michael@0: mRequest.getDatasetId(), michael@0: mRequest.getFilterDetail()); michael@0: michael@0: mRequest = newRequest; michael@0: super.onContentChanged(); michael@0: } michael@0: michael@0: @Override michael@0: public Cursor loadCursor() { michael@0: final ContentResolver cr = getContext().getContentResolver(); michael@0: michael@0: final String selection; michael@0: final String[] selectionArgs; michael@0: michael@0: // Null represents the root filter michael@0: if (mRequest.getFilter() == null) { michael@0: selection = HomeItems.FILTER + " IS NULL"; michael@0: selectionArgs = null; michael@0: } else { michael@0: selection = HomeItems.FILTER + " = ?"; michael@0: selectionArgs = new String[] { mRequest.getFilter() }; michael@0: } michael@0: michael@0: final Uri queryUri = HomeItems.CONTENT_URI.buildUpon() michael@0: .appendQueryParameter(BrowserContract.PARAM_DATASET_ID, michael@0: mRequest.getDatasetId()) michael@0: .build(); michael@0: michael@0: // XXX: You can use HomeItems.CONTENT_FAKE_URI for development michael@0: // to pull items from fake_home_items.json. michael@0: return cr.query(queryUri, null, selection, selectionArgs, null); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * LoaderCallbacks implementation that interacts with the LoaderManager. michael@0: */ michael@0: private class PanelLoaderCallbacks implements LoaderCallbacks { michael@0: @Override michael@0: public Loader onCreateLoader(int id, Bundle args) { michael@0: final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST); michael@0: michael@0: Log.d(LOGTAG, "Creating loader for request: " + request); michael@0: return new PanelDatasetLoader(getActivity(), request); michael@0: } michael@0: michael@0: @Override michael@0: public void onLoadFinished(Loader loader, Cursor cursor) { michael@0: final DatasetRequest request = getRequestFromLoader(loader); michael@0: Log.d(LOGTAG, "Finished loader for request: " + request); michael@0: michael@0: if (mPanelLayout != null) { michael@0: mPanelLayout.deliverDataset(request, cursor); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onLoaderReset(Loader loader) { michael@0: final DatasetRequest request = getRequestFromLoader(loader); michael@0: Log.d(LOGTAG, "Resetting loader for request: " + request); michael@0: michael@0: if (mPanelLayout != null) { michael@0: mPanelLayout.releaseDataset(request.getViewIndex()); michael@0: } michael@0: } michael@0: michael@0: private DatasetRequest getRequestFromLoader(Loader loader) { michael@0: final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; michael@0: return datasetLoader.getRequest(); michael@0: } michael@0: } michael@0: michael@0: private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener { michael@0: @Override michael@0: public void onChange(String panelId, boolean isAuthenticated) { michael@0: if (!mPanelConfig.getId().equals(panelId)) { michael@0: return; michael@0: } michael@0: michael@0: setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); michael@0: } michael@0: } michael@0: }