-
Notifications
You must be signed in to change notification settings - Fork 13
/
ClassifyFragment.java
402 lines (340 loc) · 14.6 KB
/
ClassifyFragment.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/*
* Copyright (C) 2014 Murray Cumming
*
* This file is part of android-galaxyzoo.
*
* android-galaxyzoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* android-galaxyzoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with android-galaxyzoo. If not, see <http://www.gnu.org/licenses/>.
*/
package com.murrayc.galaxyzoo.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import com.murrayc.galaxyzoo.app.provider.Item;
import com.murrayc.galaxyzoo.app.provider.ItemsContentProvider;
/**
* A fragment representing a single subject.
* This fragment is either contained in a {@link com.murrayc.galaxyzoo.app.ListActivity}
* in two-pane mode (on tablets) or a {@link com.murrayc.galaxyzoo.app.ClassifyActivity}
* on handsets.
*/
public class ClassifyFragment extends ItemFragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int URL_LOADER = 0;
// We have to hard-code the indices - we can't use getColumnIndex because the Cursor
// (actually a SQliteDatabase cursor returned
// from our ContentProvider) only knows about the underlying SQLite database column names,
// not our ContentProvider's column names. That seems like a design error in the Android API.
//TODO: Use org.apache.commons.lang.ArrayUtils.indexOf() instead?
private static final int COLUMN_INDEX_ID = 0;
private final String[] mColumns = {Item.Columns._ID};
private Cursor mCursor;
private View mLoadingView;
private View mRootView;
private AlertDialog mAlertDialog = null;
private boolean mGetNextInProgress = false;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public ClassifyFragment() {
}
private void warnAboutNetworkProblemWithRetry(final Activity activity, final String message) {
//Dismiss any existing dialog:
if (mAlertDialog != null) {
mAlertDialog.dismiss();
mAlertDialog = null;
}
//Show the new one:
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
// http://developer.android.com/design/building-blocks/dialogs.html
// says "Most alerts don't need titles.":
// builder.setTitle(activity.getString(R.string.error_title_connection_problem));
builder.setMessage(message);
builder.setPositiveButton(activity.getString(R.string.error_button_retry), new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, int which) {
onClickListenerRetry();
}
});
builder.setNegativeButton(activity.getString(R.string.error_button_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
mAlertDialog = null;
}
});
mAlertDialog = builder.create();
mAlertDialog.show();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.fragment_classify, container, false);
assert mRootView != null;
//Show the progress spinner while we are waiting for the subject to load,
//particularly during first start when we are waiting to get the first data in our cache.
showLoadingInProgress(true);
initializeSingleton();
return mRootView;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
createCommonOptionsMenu(menu, inflater);
}
@Override
protected void onSingletonInitialized() {
super.onSingletonInitialized();
//Now we are ready to do more:
update();
}
/** Show either the loading view (progress)
* or the child fragments, but not both,
* and not nothing.
* @param loadingInProgress
*/
private void showLoadingInProgress(boolean loadingInProgress) {
showLoadingView(loadingInProgress);
showChildFragments(loadingInProgress);
}
/** Hide both the loading (progress) view and the child fragments.
*/
private void hideAll() {
showLoadingView(false);
showChildFragments(false);
}
/** Show,, or hide, the progress spinner.
*
* @param show
*/
private void showLoadingView(boolean show) {
if (mLoadingView == null) {
mLoadingView = mRootView.findViewById(R.id.loading_spinner);
}
mLoadingView.setVisibility(show ? View.VISIBLE : View.GONE);
}
/** Show, or hide, the child fragments.
*/
private void showChildFragments(boolean show) {
//If we are showing the loading view then we should hide the other fragments,
//and vice-versa.
final FragmentManager fragmentManager = getChildFragmentManager();
final FragmentTransaction transaction = fragmentManager.beginTransaction();
final Fragment fragmentSubject = fragmentManager.findFragmentById(R.id.child_fragment_subject);
if (fragmentSubject != null) {
if (show) {
transaction.hide(fragmentSubject);
} else {
transaction.show(fragmentSubject);
}
}
final Fragment fragmentQuestion = fragmentManager.findFragmentById(R.id.child_fragment_question);
if (fragmentQuestion != null) {
if (show) {
transaction.hide(fragmentQuestion);
} else {
transaction.show(fragmentQuestion);
}
}
transaction.commit();
}
private void addOrUpdateChildFragments() {
showLoadingInProgress(false);
final Bundle arguments = new Bundle();
//TODO? arguments.putString(ARG_USER_ID,
// getUserId()); //Obtained in the super class.
arguments.putString(ItemFragment.ARG_ITEM_ID,
getItemId());
//Add, or update, the nested child fragments.
//This can only be done programmatically, not in the layout XML.
//See http://developer.android.com/about/versions/android-4.2.html#NestedFragments
final FragmentManager fragmentManager = getChildFragmentManager();
SubjectFragment fragmentSubject = (SubjectFragment) fragmentManager.findFragmentById(R.id.child_fragment_subject);
if (fragmentSubject == null) {
fragmentSubject = new SubjectFragment();
fragmentSubject.setArguments(arguments);
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.child_fragment_subject, fragmentSubject).commit();
} else {
//TODO: Is there some more standard method to do this,
//to trigger the Fragments' onCreate()?
fragmentSubject.setItemId(getItemId());
fragmentSubject.setInverted(false); //Don't stay inverted after a previous classification.
fragmentSubject.update();
}
QuestionFragment fragmentQuestion = (QuestionFragment) fragmentManager.findFragmentById(R.id.child_fragment_question);
if (fragmentQuestion == null) {
fragmentQuestion = new QuestionFragment();
fragmentQuestion.setArguments(arguments);
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.child_fragment_question, fragmentQuestion).commit();
} else {
//TODO: Is there some more standard method to do this,
//to trigger the Fragments' onCreate()?
fragmentQuestion.setItemId(getItemId());
fragmentQuestion.update();
}
}
public void update() {
final Activity activity = getActivity();
if (activity == null)
return;
if (TextUtils.equals(getItemId(), ItemsContentProvider.URI_PART_ITEM_ID_NEXT)) {
/*
* Initializes the CursorLoader. The URL_LOADER value is eventually passed
* to onCreateLoader().
* We use restartLoader(), instead of initLoader(),
* so we can refresh this fragment to show a different subject,
* even when using the same query ("next") to do that.
*
* However, we don't start another "next" request when one is already in progress,
* because then we would waste the first one and slow both down.
* This can happen during resume.
*/
if(!mGetNextInProgress) {
mGetNextInProgress = true;
getLoaderManager().restartLoader(URL_LOADER, null, this);
}
} else {
//Add, or update, the child fragments already, because we know the Item IDs:
addOrUpdateChildFragments();
}
}
/* We don't override this, to call update(),
* because that can sometimes lead to us using a Fragment Transaction at the wrong time,
* causing this exception:
* "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState".
* Instead we do it in the parent activity's onResumeFragments() - see ClassifyActivty.onResumeFragments() .
* as suggested here:
* http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
*/
/*
@Override
public void onResume() {
super.onResume();
if(TextUtils.equals(getItemId(), ItemsContentProvider.URI_PART_ITEM_ID_NEXT)) {
//We are probably resuming again after a previous failure to get new items
//from the network, so try again:
update();
}
}
*/
private void updateFromCursor() {
if (mCursor == null) {
Log.error("mCursor is null.");
return;
}
final Activity activity = getActivity();
if (activity == null)
return;
if (mCursor.getCount() <= 0) { //In case the query returned no rows.
Log.error("ClassifyFragment.updateFromCursor(): The ContentProvider query returned no rows.");
//Hide any UI that would need an actual ID,
//and don't pretend that we are still loading.
//If the user retries, of if we retry automatically later,
//we will show the loading view (progress) again.
hideAll();
if(!UiUtils.warnAboutMissingNetwork(activity)) {
//Warn that there is some other network problem.
//For instance, this happens if the network is apparently connected but not working properly:
warnAboutNetworkProblemWithRetry(activity, activity.getString(R.string.error_no_subjects));
}
return;
}
showLoadingInProgress(false);
mCursor.moveToFirst(); //There should only be one anyway.
if (mRootView == null) {
Log.error("ClassifyFragment.updateFromCursor(): mRootView is null.");
return;
}
//This will return the actual ID if we asked for the NEXT id.
if (mCursor.getCount() > 0) {
final String itemId = mCursor.getString(COLUMN_INDEX_ID);
setItemId(itemId);
}
addOrUpdateChildFragments();
}
private void onClickListenerRetry() {
//Try to get the next item again.
//It should succeed if we have a working network connection,
//or fail again with the same message.
update();
}
//We only bother using this when we have asked for the "next" item,
//because we want to know its ID.
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
if (loaderId != URL_LOADER) {
return null;
}
final String itemId = getItemId();
if (TextUtils.isEmpty(itemId)) {
return null;
}
//Asynchronously get the actual ID,
//because we have just asked for the "next" item.
final Activity activity = getActivity();
final Uri.Builder builder = Item.CONTENT_URI.buildUpon();
builder.appendPath(itemId);
showLoadingInProgress(true);
return new CursorLoader(
activity,
builder.build(),
mColumns,
null, // No where clause, return all records. We already specify just one via the itemId in the URI
null, // No where clause, therefore no where column values.
null // Use the default sort order.
);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
mCursor = cursor;
mGetNextInProgress = false;
updateFromCursor();
// Avoid this being called twice, which seems to be an Android bug,
// and which could cause us to get a different item ID if our virtual "next" item changes to
// another item:
// See http://stackoverflow.com/questions/14719814/onloadfinished-called-twice
// and https://code.google.com/p/android/issues/detail?id=63179
getLoaderManager().destroyLoader(URL_LOADER);
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
/*
* Clears out our reference to the Cursor.
* This prevents memory leaks.
*/
mCursor = null;
}
}