1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/home/DynamicPanel.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,440 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.home; 1.10 + 1.11 +import org.json.JSONException; 1.12 +import org.json.JSONObject; 1.13 + 1.14 +import org.mozilla.gecko.GeckoAppShell; 1.15 +import org.mozilla.gecko.db.BrowserContract; 1.16 +import org.mozilla.gecko.db.BrowserContract.HomeItems; 1.17 +import org.mozilla.gecko.db.DBUtils; 1.18 +import org.mozilla.gecko.db.HomeProvider; 1.19 +import org.mozilla.gecko.home.HomeConfig.PanelConfig; 1.20 +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; 1.21 +import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry; 1.22 +import org.mozilla.gecko.home.PanelLayout.DatasetHandler; 1.23 +import org.mozilla.gecko.home.PanelLayout.DatasetRequest; 1.24 +import org.mozilla.gecko.util.GeckoEventListener; 1.25 +import org.mozilla.gecko.util.ThreadUtils; 1.26 +import org.mozilla.gecko.util.UiAsyncTask; 1.27 + 1.28 +import android.app.Activity; 1.29 +import android.content.ContentResolver; 1.30 +import android.content.Context; 1.31 +import android.content.res.Configuration; 1.32 +import android.database.Cursor; 1.33 +import android.net.Uri; 1.34 +import android.os.Bundle; 1.35 +import android.support.v4.app.LoaderManager; 1.36 +import android.support.v4.app.LoaderManager.LoaderCallbacks; 1.37 +import android.support.v4.content.Loader; 1.38 +import android.util.Log; 1.39 +import android.view.LayoutInflater; 1.40 +import android.view.View; 1.41 +import android.view.ViewGroup; 1.42 +import android.widget.FrameLayout; 1.43 + 1.44 +/** 1.45 + * Fragment that displays dynamic content specified by a {@code PanelConfig}. 1.46 + * The {@code DynamicPanel} UI is built based on the given {@code LayoutType} 1.47 + * and its associated list of {@code ViewConfig}. 1.48 + * 1.49 + * {@code DynamicPanel} manages all necessary Loaders to load panel datasets 1.50 + * from their respective content providers. Each panel dataset has its own 1.51 + * associated Loader. This is enforced by defining the Loader IDs based on 1.52 + * their associated dataset IDs. 1.53 + * 1.54 + * The {@code PanelLayout} can make load and reset requests on datasets via 1.55 + * the provided {@code DatasetHandler}. This way it doesn't need to know the 1.56 + * details of how datasets are loaded and reset. Each time a dataset is 1.57 + * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see 1.58 + * {@code PanelDatasetHandler}). 1.59 + * 1.60 + * See {@code PanelLayout} for more details on how {@code DynamicPanel} 1.61 + * receives dataset requests and delivers them back to the {@code PanelLayout}. 1.62 + */ 1.63 +public class DynamicPanel extends HomeFragment { 1.64 + private static final String LOGTAG = "GeckoDynamicPanel"; 1.65 + 1.66 + // Dataset ID to be used by the loader 1.67 + private static final String DATASET_REQUEST = "dataset_request"; 1.68 + 1.69 + // The main view for this fragment. This contains the PanelLayout and PanelAuthLayout. 1.70 + private FrameLayout mView; 1.71 + 1.72 + // The panel layout associated with this panel 1.73 + private PanelLayout mPanelLayout; 1.74 + 1.75 + // The layout used to show authentication UI for this panel 1.76 + private PanelAuthLayout mPanelAuthLayout; 1.77 + 1.78 + // Cache used to keep track of whether or not the user has been authenticated. 1.79 + private PanelAuthCache mPanelAuthCache; 1.80 + 1.81 + // Hold a reference to the UiAsyncTask we use to check the state of the 1.82 + // PanelAuthCache, so that we can cancel it if necessary. 1.83 + private UiAsyncTask<Void, Void, Boolean> mAuthStateTask; 1.84 + 1.85 + // The configuration associated with this panel 1.86 + private PanelConfig mPanelConfig; 1.87 + 1.88 + // Callbacks used for the loader 1.89 + private PanelLoaderCallbacks mLoaderCallbacks; 1.90 + 1.91 + // On URL open listener 1.92 + private OnUrlOpenListener mUrlOpenListener; 1.93 + 1.94 + // The current UI mode in the fragment 1.95 + private UIMode mUIMode; 1.96 + 1.97 + /* 1.98 + * Different UI modes to display depending on the authentication state. 1.99 + * 1.100 + * PANEL: Layout to display panel data. 1.101 + * AUTH: Authentication UI. 1.102 + */ 1.103 + private enum UIMode { 1.104 + PANEL, 1.105 + AUTH 1.106 + } 1.107 + 1.108 + @Override 1.109 + public void onAttach(Activity activity) { 1.110 + super.onAttach(activity); 1.111 + 1.112 + try { 1.113 + mUrlOpenListener = (OnUrlOpenListener) activity; 1.114 + } catch (ClassCastException e) { 1.115 + throw new ClassCastException(activity.toString() 1.116 + + " must implement HomePager.OnUrlOpenListener"); 1.117 + } 1.118 + } 1.119 + 1.120 + @Override 1.121 + public void onDetach() { 1.122 + super.onDetach(); 1.123 + 1.124 + mUrlOpenListener = null; 1.125 + } 1.126 + 1.127 + @Override 1.128 + public void onCreate(Bundle savedInstanceState) { 1.129 + super.onCreate(savedInstanceState); 1.130 + 1.131 + final Bundle args = getArguments(); 1.132 + if (args != null) { 1.133 + mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG); 1.134 + } 1.135 + 1.136 + if (mPanelConfig == null) { 1.137 + throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig"); 1.138 + } 1.139 + 1.140 + mPanelAuthCache = new PanelAuthCache(getActivity()); 1.141 + } 1.142 + 1.143 + @Override 1.144 + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 1.145 + mView = new FrameLayout(getActivity()); 1.146 + return mView; 1.147 + } 1.148 + 1.149 + @Override 1.150 + public void onViewCreated(View view, Bundle savedInstanceState) { 1.151 + super.onViewCreated(view, savedInstanceState); 1.152 + 1.153 + // Restore whatever the UI mode the fragment had before 1.154 + // a device rotation. 1.155 + if (mUIMode != null) { 1.156 + setUIMode(mUIMode); 1.157 + } 1.158 + 1.159 + mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener()); 1.160 + } 1.161 + 1.162 + @Override 1.163 + public void onDestroyView() { 1.164 + super.onDestroyView(); 1.165 + mView = null; 1.166 + mPanelLayout = null; 1.167 + mPanelAuthLayout = null; 1.168 + 1.169 + mPanelAuthCache.setOnChangeListener(null); 1.170 + 1.171 + if (mAuthStateTask != null) { 1.172 + mAuthStateTask.cancel(true); 1.173 + mAuthStateTask = null; 1.174 + } 1.175 + } 1.176 + 1.177 + @Override 1.178 + public void onConfigurationChanged(Configuration newConfig) { 1.179 + super.onConfigurationChanged(newConfig); 1.180 + 1.181 + // Detach and reattach the fragment as the layout changes. 1.182 + if (isVisible()) { 1.183 + getFragmentManager().beginTransaction() 1.184 + .detach(this) 1.185 + .attach(this) 1.186 + .commitAllowingStateLoss(); 1.187 + } 1.188 + } 1.189 + 1.190 + @Override 1.191 + public void onActivityCreated(Bundle savedInstanceState) { 1.192 + super.onActivityCreated(savedInstanceState); 1.193 + 1.194 + // Create callbacks before the initial loader is started. 1.195 + mLoaderCallbacks = new PanelLoaderCallbacks(); 1.196 + loadIfVisible(); 1.197 + } 1.198 + 1.199 + @Override 1.200 + protected void load() { 1.201 + Log.d(LOGTAG, "Loading layout"); 1.202 + 1.203 + if (requiresAuth()) { 1.204 + mAuthStateTask = new UiAsyncTask<Void, Void, Boolean>(ThreadUtils.getBackgroundHandler()) { 1.205 + @Override 1.206 + public synchronized Boolean doInBackground(Void... params) { 1.207 + return mPanelAuthCache.isAuthenticated(mPanelConfig.getId()); 1.208 + } 1.209 + 1.210 + @Override 1.211 + public void onPostExecute(Boolean isAuthenticated) { 1.212 + mAuthStateTask = null; 1.213 + setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); 1.214 + } 1.215 + }; 1.216 + mAuthStateTask.execute(); 1.217 + } else { 1.218 + setUIMode(UIMode.PANEL); 1.219 + } 1.220 + } 1.221 + 1.222 + /** 1.223 + * @return true if this panel requires authentication. 1.224 + */ 1.225 + private boolean requiresAuth() { 1.226 + return mPanelConfig.getAuthConfig() != null; 1.227 + } 1.228 + 1.229 + /** 1.230 + * Lazily creates layout for panel data. 1.231 + */ 1.232 + private void createPanelLayout() { 1.233 + final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() { 1.234 + @Override 1.235 + public void register(View view) { 1.236 + registerForContextMenu(view); 1.237 + } 1.238 + }; 1.239 + 1.240 + switch(mPanelConfig.getLayoutType()) { 1.241 + case FRAME: 1.242 + final PanelDatasetHandler datasetHandler = new PanelDatasetHandler(); 1.243 + mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler, 1.244 + mUrlOpenListener, contextMenuRegistry); 1.245 + break; 1.246 + 1.247 + default: 1.248 + throw new IllegalStateException("Unrecognized layout type in DynamicPanel"); 1.249 + } 1.250 + 1.251 + Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType()); 1.252 + mView.addView(mPanelLayout); 1.253 + } 1.254 + 1.255 + /** 1.256 + * Lazily creates layout for authentication UI. 1.257 + */ 1.258 + private void createPanelAuthLayout() { 1.259 + mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig); 1.260 + mView.addView(mPanelAuthLayout, 0); 1.261 + } 1.262 + 1.263 + private void setUIMode(UIMode mode) { 1.264 + switch(mode) { 1.265 + case PANEL: 1.266 + if (mPanelAuthLayout != null) { 1.267 + mPanelAuthLayout.setVisibility(View.GONE); 1.268 + } 1.269 + if (mPanelLayout == null) { 1.270 + createPanelLayout(); 1.271 + } 1.272 + mPanelLayout.setVisibility(View.VISIBLE); 1.273 + 1.274 + // Only trigger a reload if the UI mode has changed 1.275 + // (e.g. auth cache changes) and the fragment is allowed 1.276 + // to load its contents. Any loaders associated with the 1.277 + // panel layout will be automatically re-bound after a 1.278 + // device rotation, no need to explicitly load it again. 1.279 + if (mUIMode != mode && canLoad()) { 1.280 + mPanelLayout.load(); 1.281 + } 1.282 + break; 1.283 + 1.284 + case AUTH: 1.285 + if (mPanelLayout != null) { 1.286 + mPanelLayout.setVisibility(View.GONE); 1.287 + } 1.288 + if (mPanelAuthLayout == null) { 1.289 + createPanelAuthLayout(); 1.290 + } 1.291 + mPanelAuthLayout.setVisibility(View.VISIBLE); 1.292 + break; 1.293 + 1.294 + default: 1.295 + throw new IllegalStateException("Unrecognized UIMode in DynamicPanel"); 1.296 + } 1.297 + 1.298 + mUIMode = mode; 1.299 + } 1.300 + 1.301 + /** 1.302 + * Used by the PanelLayout to make load and reset requests to 1.303 + * the holding fragment. 1.304 + */ 1.305 + private class PanelDatasetHandler implements DatasetHandler { 1.306 + @Override 1.307 + public void requestDataset(DatasetRequest request) { 1.308 + Log.d(LOGTAG, "Requesting request: " + request); 1.309 + 1.310 + // Ignore dataset requests while the fragment is not 1.311 + // allowed to load its content. 1.312 + if (!getCanLoadHint()) { 1.313 + return; 1.314 + } 1.315 + 1.316 + final Bundle bundle = new Bundle(); 1.317 + bundle.putParcelable(DATASET_REQUEST, request); 1.318 + 1.319 + getLoaderManager().restartLoader(request.getViewIndex(), 1.320 + bundle, mLoaderCallbacks); 1.321 + } 1.322 + 1.323 + @Override 1.324 + public void resetDataset(int viewIndex) { 1.325 + Log.d(LOGTAG, "Resetting dataset: " + viewIndex); 1.326 + 1.327 + final LoaderManager lm = getLoaderManager(); 1.328 + 1.329 + // Release any resources associated with the dataset if 1.330 + // it's currently loaded in memory. 1.331 + final Loader<?> datasetLoader = lm.getLoader(viewIndex); 1.332 + if (datasetLoader != null) { 1.333 + datasetLoader.reset(); 1.334 + } 1.335 + } 1.336 + } 1.337 + 1.338 + /** 1.339 + * Cursor loader for the panel datasets. 1.340 + */ 1.341 + private static class PanelDatasetLoader extends SimpleCursorLoader { 1.342 + private DatasetRequest mRequest; 1.343 + 1.344 + public PanelDatasetLoader(Context context, DatasetRequest request) { 1.345 + super(context); 1.346 + mRequest = request; 1.347 + } 1.348 + 1.349 + public DatasetRequest getRequest() { 1.350 + return mRequest; 1.351 + } 1.352 + 1.353 + @Override 1.354 + public void onContentChanged() { 1.355 + // Ensure the refresh request doesn't affect the view's filter 1.356 + // stack (i.e. use DATASET_LOAD type) but keep the current 1.357 + // dataset ID and filter. 1.358 + final DatasetRequest newRequest = 1.359 + new DatasetRequest(mRequest.getViewIndex(), 1.360 + DatasetRequest.Type.DATASET_LOAD, 1.361 + mRequest.getDatasetId(), 1.362 + mRequest.getFilterDetail()); 1.363 + 1.364 + mRequest = newRequest; 1.365 + super.onContentChanged(); 1.366 + } 1.367 + 1.368 + @Override 1.369 + public Cursor loadCursor() { 1.370 + final ContentResolver cr = getContext().getContentResolver(); 1.371 + 1.372 + final String selection; 1.373 + final String[] selectionArgs; 1.374 + 1.375 + // Null represents the root filter 1.376 + if (mRequest.getFilter() == null) { 1.377 + selection = HomeItems.FILTER + " IS NULL"; 1.378 + selectionArgs = null; 1.379 + } else { 1.380 + selection = HomeItems.FILTER + " = ?"; 1.381 + selectionArgs = new String[] { mRequest.getFilter() }; 1.382 + } 1.383 + 1.384 + final Uri queryUri = HomeItems.CONTENT_URI.buildUpon() 1.385 + .appendQueryParameter(BrowserContract.PARAM_DATASET_ID, 1.386 + mRequest.getDatasetId()) 1.387 + .build(); 1.388 + 1.389 + // XXX: You can use HomeItems.CONTENT_FAKE_URI for development 1.390 + // to pull items from fake_home_items.json. 1.391 + return cr.query(queryUri, null, selection, selectionArgs, null); 1.392 + } 1.393 + } 1.394 + 1.395 + /** 1.396 + * LoaderCallbacks implementation that interacts with the LoaderManager. 1.397 + */ 1.398 + private class PanelLoaderCallbacks implements LoaderCallbacks<Cursor> { 1.399 + @Override 1.400 + public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1.401 + final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST); 1.402 + 1.403 + Log.d(LOGTAG, "Creating loader for request: " + request); 1.404 + return new PanelDatasetLoader(getActivity(), request); 1.405 + } 1.406 + 1.407 + @Override 1.408 + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 1.409 + final DatasetRequest request = getRequestFromLoader(loader); 1.410 + Log.d(LOGTAG, "Finished loader for request: " + request); 1.411 + 1.412 + if (mPanelLayout != null) { 1.413 + mPanelLayout.deliverDataset(request, cursor); 1.414 + } 1.415 + } 1.416 + 1.417 + @Override 1.418 + public void onLoaderReset(Loader<Cursor> loader) { 1.419 + final DatasetRequest request = getRequestFromLoader(loader); 1.420 + Log.d(LOGTAG, "Resetting loader for request: " + request); 1.421 + 1.422 + if (mPanelLayout != null) { 1.423 + mPanelLayout.releaseDataset(request.getViewIndex()); 1.424 + } 1.425 + } 1.426 + 1.427 + private DatasetRequest getRequestFromLoader(Loader<Cursor> loader) { 1.428 + final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; 1.429 + return datasetLoader.getRequest(); 1.430 + } 1.431 + } 1.432 + 1.433 + private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener { 1.434 + @Override 1.435 + public void onChange(String panelId, boolean isAuthenticated) { 1.436 + if (!mPanelConfig.getId().equals(panelId)) { 1.437 + return; 1.438 + } 1.439 + 1.440 + setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); 1.441 + } 1.442 + } 1.443 +}