|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.home; |
|
7 |
|
8 import org.json.JSONException; |
|
9 import org.json.JSONObject; |
|
10 |
|
11 import org.mozilla.gecko.GeckoAppShell; |
|
12 import org.mozilla.gecko.db.BrowserContract; |
|
13 import org.mozilla.gecko.db.BrowserContract.HomeItems; |
|
14 import org.mozilla.gecko.db.DBUtils; |
|
15 import org.mozilla.gecko.db.HomeProvider; |
|
16 import org.mozilla.gecko.home.HomeConfig.PanelConfig; |
|
17 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; |
|
18 import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry; |
|
19 import org.mozilla.gecko.home.PanelLayout.DatasetHandler; |
|
20 import org.mozilla.gecko.home.PanelLayout.DatasetRequest; |
|
21 import org.mozilla.gecko.util.GeckoEventListener; |
|
22 import org.mozilla.gecko.util.ThreadUtils; |
|
23 import org.mozilla.gecko.util.UiAsyncTask; |
|
24 |
|
25 import android.app.Activity; |
|
26 import android.content.ContentResolver; |
|
27 import android.content.Context; |
|
28 import android.content.res.Configuration; |
|
29 import android.database.Cursor; |
|
30 import android.net.Uri; |
|
31 import android.os.Bundle; |
|
32 import android.support.v4.app.LoaderManager; |
|
33 import android.support.v4.app.LoaderManager.LoaderCallbacks; |
|
34 import android.support.v4.content.Loader; |
|
35 import android.util.Log; |
|
36 import android.view.LayoutInflater; |
|
37 import android.view.View; |
|
38 import android.view.ViewGroup; |
|
39 import android.widget.FrameLayout; |
|
40 |
|
41 /** |
|
42 * Fragment that displays dynamic content specified by a {@code PanelConfig}. |
|
43 * The {@code DynamicPanel} UI is built based on the given {@code LayoutType} |
|
44 * and its associated list of {@code ViewConfig}. |
|
45 * |
|
46 * {@code DynamicPanel} manages all necessary Loaders to load panel datasets |
|
47 * from their respective content providers. Each panel dataset has its own |
|
48 * associated Loader. This is enforced by defining the Loader IDs based on |
|
49 * their associated dataset IDs. |
|
50 * |
|
51 * The {@code PanelLayout} can make load and reset requests on datasets via |
|
52 * the provided {@code DatasetHandler}. This way it doesn't need to know the |
|
53 * details of how datasets are loaded and reset. Each time a dataset is |
|
54 * requested, {@code DynamicPanel} restarts a Loader with the respective ID (see |
|
55 * {@code PanelDatasetHandler}). |
|
56 * |
|
57 * See {@code PanelLayout} for more details on how {@code DynamicPanel} |
|
58 * receives dataset requests and delivers them back to the {@code PanelLayout}. |
|
59 */ |
|
60 public class DynamicPanel extends HomeFragment { |
|
61 private static final String LOGTAG = "GeckoDynamicPanel"; |
|
62 |
|
63 // Dataset ID to be used by the loader |
|
64 private static final String DATASET_REQUEST = "dataset_request"; |
|
65 |
|
66 // The main view for this fragment. This contains the PanelLayout and PanelAuthLayout. |
|
67 private FrameLayout mView; |
|
68 |
|
69 // The panel layout associated with this panel |
|
70 private PanelLayout mPanelLayout; |
|
71 |
|
72 // The layout used to show authentication UI for this panel |
|
73 private PanelAuthLayout mPanelAuthLayout; |
|
74 |
|
75 // Cache used to keep track of whether or not the user has been authenticated. |
|
76 private PanelAuthCache mPanelAuthCache; |
|
77 |
|
78 // Hold a reference to the UiAsyncTask we use to check the state of the |
|
79 // PanelAuthCache, so that we can cancel it if necessary. |
|
80 private UiAsyncTask<Void, Void, Boolean> mAuthStateTask; |
|
81 |
|
82 // The configuration associated with this panel |
|
83 private PanelConfig mPanelConfig; |
|
84 |
|
85 // Callbacks used for the loader |
|
86 private PanelLoaderCallbacks mLoaderCallbacks; |
|
87 |
|
88 // On URL open listener |
|
89 private OnUrlOpenListener mUrlOpenListener; |
|
90 |
|
91 // The current UI mode in the fragment |
|
92 private UIMode mUIMode; |
|
93 |
|
94 /* |
|
95 * Different UI modes to display depending on the authentication state. |
|
96 * |
|
97 * PANEL: Layout to display panel data. |
|
98 * AUTH: Authentication UI. |
|
99 */ |
|
100 private enum UIMode { |
|
101 PANEL, |
|
102 AUTH |
|
103 } |
|
104 |
|
105 @Override |
|
106 public void onAttach(Activity activity) { |
|
107 super.onAttach(activity); |
|
108 |
|
109 try { |
|
110 mUrlOpenListener = (OnUrlOpenListener) activity; |
|
111 } catch (ClassCastException e) { |
|
112 throw new ClassCastException(activity.toString() |
|
113 + " must implement HomePager.OnUrlOpenListener"); |
|
114 } |
|
115 } |
|
116 |
|
117 @Override |
|
118 public void onDetach() { |
|
119 super.onDetach(); |
|
120 |
|
121 mUrlOpenListener = null; |
|
122 } |
|
123 |
|
124 @Override |
|
125 public void onCreate(Bundle savedInstanceState) { |
|
126 super.onCreate(savedInstanceState); |
|
127 |
|
128 final Bundle args = getArguments(); |
|
129 if (args != null) { |
|
130 mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG); |
|
131 } |
|
132 |
|
133 if (mPanelConfig == null) { |
|
134 throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig"); |
|
135 } |
|
136 |
|
137 mPanelAuthCache = new PanelAuthCache(getActivity()); |
|
138 } |
|
139 |
|
140 @Override |
|
141 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
|
142 mView = new FrameLayout(getActivity()); |
|
143 return mView; |
|
144 } |
|
145 |
|
146 @Override |
|
147 public void onViewCreated(View view, Bundle savedInstanceState) { |
|
148 super.onViewCreated(view, savedInstanceState); |
|
149 |
|
150 // Restore whatever the UI mode the fragment had before |
|
151 // a device rotation. |
|
152 if (mUIMode != null) { |
|
153 setUIMode(mUIMode); |
|
154 } |
|
155 |
|
156 mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener()); |
|
157 } |
|
158 |
|
159 @Override |
|
160 public void onDestroyView() { |
|
161 super.onDestroyView(); |
|
162 mView = null; |
|
163 mPanelLayout = null; |
|
164 mPanelAuthLayout = null; |
|
165 |
|
166 mPanelAuthCache.setOnChangeListener(null); |
|
167 |
|
168 if (mAuthStateTask != null) { |
|
169 mAuthStateTask.cancel(true); |
|
170 mAuthStateTask = null; |
|
171 } |
|
172 } |
|
173 |
|
174 @Override |
|
175 public void onConfigurationChanged(Configuration newConfig) { |
|
176 super.onConfigurationChanged(newConfig); |
|
177 |
|
178 // Detach and reattach the fragment as the layout changes. |
|
179 if (isVisible()) { |
|
180 getFragmentManager().beginTransaction() |
|
181 .detach(this) |
|
182 .attach(this) |
|
183 .commitAllowingStateLoss(); |
|
184 } |
|
185 } |
|
186 |
|
187 @Override |
|
188 public void onActivityCreated(Bundle savedInstanceState) { |
|
189 super.onActivityCreated(savedInstanceState); |
|
190 |
|
191 // Create callbacks before the initial loader is started. |
|
192 mLoaderCallbacks = new PanelLoaderCallbacks(); |
|
193 loadIfVisible(); |
|
194 } |
|
195 |
|
196 @Override |
|
197 protected void load() { |
|
198 Log.d(LOGTAG, "Loading layout"); |
|
199 |
|
200 if (requiresAuth()) { |
|
201 mAuthStateTask = new UiAsyncTask<Void, Void, Boolean>(ThreadUtils.getBackgroundHandler()) { |
|
202 @Override |
|
203 public synchronized Boolean doInBackground(Void... params) { |
|
204 return mPanelAuthCache.isAuthenticated(mPanelConfig.getId()); |
|
205 } |
|
206 |
|
207 @Override |
|
208 public void onPostExecute(Boolean isAuthenticated) { |
|
209 mAuthStateTask = null; |
|
210 setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); |
|
211 } |
|
212 }; |
|
213 mAuthStateTask.execute(); |
|
214 } else { |
|
215 setUIMode(UIMode.PANEL); |
|
216 } |
|
217 } |
|
218 |
|
219 /** |
|
220 * @return true if this panel requires authentication. |
|
221 */ |
|
222 private boolean requiresAuth() { |
|
223 return mPanelConfig.getAuthConfig() != null; |
|
224 } |
|
225 |
|
226 /** |
|
227 * Lazily creates layout for panel data. |
|
228 */ |
|
229 private void createPanelLayout() { |
|
230 final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() { |
|
231 @Override |
|
232 public void register(View view) { |
|
233 registerForContextMenu(view); |
|
234 } |
|
235 }; |
|
236 |
|
237 switch(mPanelConfig.getLayoutType()) { |
|
238 case FRAME: |
|
239 final PanelDatasetHandler datasetHandler = new PanelDatasetHandler(); |
|
240 mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler, |
|
241 mUrlOpenListener, contextMenuRegistry); |
|
242 break; |
|
243 |
|
244 default: |
|
245 throw new IllegalStateException("Unrecognized layout type in DynamicPanel"); |
|
246 } |
|
247 |
|
248 Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType()); |
|
249 mView.addView(mPanelLayout); |
|
250 } |
|
251 |
|
252 /** |
|
253 * Lazily creates layout for authentication UI. |
|
254 */ |
|
255 private void createPanelAuthLayout() { |
|
256 mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig); |
|
257 mView.addView(mPanelAuthLayout, 0); |
|
258 } |
|
259 |
|
260 private void setUIMode(UIMode mode) { |
|
261 switch(mode) { |
|
262 case PANEL: |
|
263 if (mPanelAuthLayout != null) { |
|
264 mPanelAuthLayout.setVisibility(View.GONE); |
|
265 } |
|
266 if (mPanelLayout == null) { |
|
267 createPanelLayout(); |
|
268 } |
|
269 mPanelLayout.setVisibility(View.VISIBLE); |
|
270 |
|
271 // Only trigger a reload if the UI mode has changed |
|
272 // (e.g. auth cache changes) and the fragment is allowed |
|
273 // to load its contents. Any loaders associated with the |
|
274 // panel layout will be automatically re-bound after a |
|
275 // device rotation, no need to explicitly load it again. |
|
276 if (mUIMode != mode && canLoad()) { |
|
277 mPanelLayout.load(); |
|
278 } |
|
279 break; |
|
280 |
|
281 case AUTH: |
|
282 if (mPanelLayout != null) { |
|
283 mPanelLayout.setVisibility(View.GONE); |
|
284 } |
|
285 if (mPanelAuthLayout == null) { |
|
286 createPanelAuthLayout(); |
|
287 } |
|
288 mPanelAuthLayout.setVisibility(View.VISIBLE); |
|
289 break; |
|
290 |
|
291 default: |
|
292 throw new IllegalStateException("Unrecognized UIMode in DynamicPanel"); |
|
293 } |
|
294 |
|
295 mUIMode = mode; |
|
296 } |
|
297 |
|
298 /** |
|
299 * Used by the PanelLayout to make load and reset requests to |
|
300 * the holding fragment. |
|
301 */ |
|
302 private class PanelDatasetHandler implements DatasetHandler { |
|
303 @Override |
|
304 public void requestDataset(DatasetRequest request) { |
|
305 Log.d(LOGTAG, "Requesting request: " + request); |
|
306 |
|
307 // Ignore dataset requests while the fragment is not |
|
308 // allowed to load its content. |
|
309 if (!getCanLoadHint()) { |
|
310 return; |
|
311 } |
|
312 |
|
313 final Bundle bundle = new Bundle(); |
|
314 bundle.putParcelable(DATASET_REQUEST, request); |
|
315 |
|
316 getLoaderManager().restartLoader(request.getViewIndex(), |
|
317 bundle, mLoaderCallbacks); |
|
318 } |
|
319 |
|
320 @Override |
|
321 public void resetDataset(int viewIndex) { |
|
322 Log.d(LOGTAG, "Resetting dataset: " + viewIndex); |
|
323 |
|
324 final LoaderManager lm = getLoaderManager(); |
|
325 |
|
326 // Release any resources associated with the dataset if |
|
327 // it's currently loaded in memory. |
|
328 final Loader<?> datasetLoader = lm.getLoader(viewIndex); |
|
329 if (datasetLoader != null) { |
|
330 datasetLoader.reset(); |
|
331 } |
|
332 } |
|
333 } |
|
334 |
|
335 /** |
|
336 * Cursor loader for the panel datasets. |
|
337 */ |
|
338 private static class PanelDatasetLoader extends SimpleCursorLoader { |
|
339 private DatasetRequest mRequest; |
|
340 |
|
341 public PanelDatasetLoader(Context context, DatasetRequest request) { |
|
342 super(context); |
|
343 mRequest = request; |
|
344 } |
|
345 |
|
346 public DatasetRequest getRequest() { |
|
347 return mRequest; |
|
348 } |
|
349 |
|
350 @Override |
|
351 public void onContentChanged() { |
|
352 // Ensure the refresh request doesn't affect the view's filter |
|
353 // stack (i.e. use DATASET_LOAD type) but keep the current |
|
354 // dataset ID and filter. |
|
355 final DatasetRequest newRequest = |
|
356 new DatasetRequest(mRequest.getViewIndex(), |
|
357 DatasetRequest.Type.DATASET_LOAD, |
|
358 mRequest.getDatasetId(), |
|
359 mRequest.getFilterDetail()); |
|
360 |
|
361 mRequest = newRequest; |
|
362 super.onContentChanged(); |
|
363 } |
|
364 |
|
365 @Override |
|
366 public Cursor loadCursor() { |
|
367 final ContentResolver cr = getContext().getContentResolver(); |
|
368 |
|
369 final String selection; |
|
370 final String[] selectionArgs; |
|
371 |
|
372 // Null represents the root filter |
|
373 if (mRequest.getFilter() == null) { |
|
374 selection = HomeItems.FILTER + " IS NULL"; |
|
375 selectionArgs = null; |
|
376 } else { |
|
377 selection = HomeItems.FILTER + " = ?"; |
|
378 selectionArgs = new String[] { mRequest.getFilter() }; |
|
379 } |
|
380 |
|
381 final Uri queryUri = HomeItems.CONTENT_URI.buildUpon() |
|
382 .appendQueryParameter(BrowserContract.PARAM_DATASET_ID, |
|
383 mRequest.getDatasetId()) |
|
384 .build(); |
|
385 |
|
386 // XXX: You can use HomeItems.CONTENT_FAKE_URI for development |
|
387 // to pull items from fake_home_items.json. |
|
388 return cr.query(queryUri, null, selection, selectionArgs, null); |
|
389 } |
|
390 } |
|
391 |
|
392 /** |
|
393 * LoaderCallbacks implementation that interacts with the LoaderManager. |
|
394 */ |
|
395 private class PanelLoaderCallbacks implements LoaderCallbacks<Cursor> { |
|
396 @Override |
|
397 public Loader<Cursor> onCreateLoader(int id, Bundle args) { |
|
398 final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST); |
|
399 |
|
400 Log.d(LOGTAG, "Creating loader for request: " + request); |
|
401 return new PanelDatasetLoader(getActivity(), request); |
|
402 } |
|
403 |
|
404 @Override |
|
405 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { |
|
406 final DatasetRequest request = getRequestFromLoader(loader); |
|
407 Log.d(LOGTAG, "Finished loader for request: " + request); |
|
408 |
|
409 if (mPanelLayout != null) { |
|
410 mPanelLayout.deliverDataset(request, cursor); |
|
411 } |
|
412 } |
|
413 |
|
414 @Override |
|
415 public void onLoaderReset(Loader<Cursor> loader) { |
|
416 final DatasetRequest request = getRequestFromLoader(loader); |
|
417 Log.d(LOGTAG, "Resetting loader for request: " + request); |
|
418 |
|
419 if (mPanelLayout != null) { |
|
420 mPanelLayout.releaseDataset(request.getViewIndex()); |
|
421 } |
|
422 } |
|
423 |
|
424 private DatasetRequest getRequestFromLoader(Loader<Cursor> loader) { |
|
425 final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader; |
|
426 return datasetLoader.getRequest(); |
|
427 } |
|
428 } |
|
429 |
|
430 private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener { |
|
431 @Override |
|
432 public void onChange(String panelId, boolean isAuthenticated) { |
|
433 if (!mPanelConfig.getId().equals(panelId)) { |
|
434 return; |
|
435 } |
|
436 |
|
437 setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH); |
|
438 } |
|
439 } |
|
440 } |