1
/*
2
* Copyright 2013 The Android Open Source Project
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
package com.example.android.basicsyncadapter;
18
19
import android.accounts.Account;
20
import android.annotation.TargetApi;
21
import android.app.Activity;
22
import android.content.ContentResolver;
23
import android.content.Intent;
24
import android.content.SyncStatusObserver;
25
import android.database.Cursor;
26
import android.net.Uri;
27
import android.os.Build;
28
import android.os.Bundle;
29
import android.support.v4.app.ListFragment;
30
import android.support.v4.app.LoaderManager;
31
import android.support.v4.content.CursorLoader;
32
import android.support.v4.content.Loader;
33
import android.support.v4.widget.SimpleCursorAdapter;
34
import android.text.format.Time;
35
import android.util.Log;
36
import android.view.Menu;
37
import android.view.MenuInflater;
38
import android.view.MenuItem;
39
import android.view.View;
40
import android.widget.ListView;
41
import android.widget.TextView;
42
43
import com.example.android.common.accounts.GenericAccountService;
44
import com.example.android.basicsyncadapter.provider.FeedContract;
45
46
/**
47
* List fragment containing a list of Atom entry objects (articles) stored in the local database.
48
*
49
* <p>Database access is mediated by a content provider, specified in
50
* {@link com.example.android.basicsyncadapter.provider.FeedProvider}. This content
51
* provider is
52
* automatically populated by {@link SyncService}.
53
*
54
* <p>Selecting an item from the displayed list displays the article in the default browser.
55
*
56
* <p>If the content provider doesn't return any data, then the first sync hasn't run yet. This sync
57
* adapter assumes data exists in the provider once a sync has run. If your app doesn't work like
58
* this, you should add a flag that notes if a sync has run, so you can differentiate between "no
59
* available data" and "no initial sync", and display this in the UI.
60
*
61
* <p>The ActionBar displays a "Refresh" button. When the user clicks "Refresh", the sync adapter
62
* runs immediately. An indeterminate ProgressBar element is displayed, showing that the sync is
63
* occurring.
64
*/
65
public class EntryListFragment extends ListFragment
66
implements LoaderManager.LoaderCallbacks<Cursor> {
67
68
private static final String TAG = "EntryListFragment";
69
70
/**
71
* Cursor adapter for controlling ListView results.
72
*/
73
private SimpleCursorAdapter mAdapter;
74
75
/**
76
* Handle to a SyncObserver. The ProgressBar element is visible until the SyncObserver reports
77
* that the sync is complete.
78
*
79
* <p>This allows us to delete our SyncObserver once the application is no longer in the
80
* foreground.
81
*/
82
private Object mSyncObserverHandle;
83
84
/**
85
* Options menu used to populate ActionBar.
86
*/
87
private Menu mOptionsMenu;
88
89
/**
90
* Projection for querying the content provider.
91
*/
92
private static final String[] PROJECTION = new String[]{
93
FeedContract.Entry._ID,
94
FeedContract.Entry.COLUMN_NAME_TITLE,
95
FeedContract.Entry.COLUMN_NAME_LINK,
96
FeedContract.Entry.COLUMN_NAME_PUBLISHED
97
};
98
99
// Column indexes. The index of a column in the Cursor is the same as its relative position in
100
// the projection.
101
/** Column index for _ID */
102
private static final int COLUMN_ID = 0;
103
/** Column index for title */
104
private static final int COLUMN_TITLE = 1;
105
/** Column index for link */
106
private static final int COLUMN_URL_STRING = 2;
107
/** Column index for published */
108
private static final int COLUMN_PUBLISHED = 3;
109
110
/**
111
* List of Cursor columns to read from when preparing an adapter to populate the ListView.
112
*/
113
private static final String[] FROM_COLUMNS = new String[]{
114
FeedContract.Entry.COLUMN_NAME_TITLE,
115
FeedContract.Entry.COLUMN_NAME_PUBLISHED
116
};
117
118
/**
119
* List of Views which will be populated by Cursor data.
120
*/
121
private static final int[] TO_FIELDS = new int[]{
122
android.R.id.text1,
123
android.R.id.text2};
124
125
/**
126
* Mandatory empty constructor for the fragment manager to instantiate the
127
* fragment (e.g. upon screen orientation changes).
128
*/
129
public EntryListFragment() {}
130
131
@Override
132
public void onCreate(Bundle savedInstanceState) {
133
super.onCreate(savedInstanceState);
134
setHasOptionsMenu(true);
135
}
136
137
/**
138
* Create SyncAccount at launch, if needed.
139
*
140
* <p>This will create a new account with the system for our application, register our
141
* {@link SyncService} with it, and establish a sync schedule.
142
*/
143
@Override
144
public void onAttach(Activity activity) {
145
super.onAttach(activity);
146
147
// Create account, if needed
148
SyncUtils.CreateSyncAccount(activity);
149
}
150
151
@Override
152
public void onViewCreated(View view, Bundle savedInstanceState) {
153
super.onViewCreated(view, savedInstanceState);
154
155
mAdapter = new SimpleCursorAdapter(
156
getActivity(), // Current context
157
android.R.layout.simple_list_item_activated_2, // Layout for individual rows
158
null, // Cursor
159
FROM_COLUMNS, // Cursor columns to use
160
TO_FIELDS, // Layout fields to use
161
0 // No flags
162
);
163
mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
164
@Override
165
public boolean setViewValue(View view, Cursor cursor, int i) {
166
if (i == COLUMN_PUBLISHED) {
167
// Convert timestamp to human-readable date
168
Time t = new Time();
169
t.set(cursor.getLong(i));
170
((TextView) view).setText(t.format("%Y-%m-%d %H:%M"));
171
return true;
172
} else {
173
// Let SimpleCursorAdapter handle other fields automatically
174
return false;
175
}
176
}
177
});
178
setListAdapter(mAdapter);
179
setEmptyText(getText(R.string.loading));
180
getLoaderManager().initLoader(0, null, this);
181
}
182
183
@Override
184
public void onResume() {
185
super.onResume();
186
mSyncStatusObserver.onStatusChanged(0);
187
188
// Watch for sync state changes
189
final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING |
190
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
191
mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver);
192
}
193
194
@Override
195
public void onPause() {
196
super.onPause();
197
if (mSyncObserverHandle != null) {
198
ContentResolver.removeStatusChangeListener(mSyncObserverHandle);
199
mSyncObserverHandle = null;
200
}
201
}
202
203
/**
204
* Query the content provider for data.
205
*
206
* <p>Loaders do queries in a background thread. They also provide a ContentObserver that is
207
* triggered when data in the content provider changes. When the sync adapter updates the
208
* content provider, the ContentObserver responds by resetting the loader and then reloading
209
* it.
210
*/
211
@Override
212
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
213
// We only have one loader, so we can ignore the value of i.
214
// (It'll be '0', as set in onCreate().)
215
return new CursorLoader(getActivity(), // Context
216
FeedContract.Entry.CONTENT_URI, // URI
217
PROJECTION, // Projection
218
null, // Selection
219
null, // Selection args
220
FeedContract.Entry.COLUMN_NAME_PUBLISHED + " desc"); // Sort
221
}
222
223
/**
224
* Move the Cursor returned by the query into the ListView adapter. This refreshes the existing
225
* UI with the data in the Cursor.
226
*/
227
@Override
228
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
229
mAdapter.changeCursor(cursor);
230
}
231
232
/**
233
* Called when the ContentObserver defined for the content provider detects that data has
234
* changed. The ContentObserver resets the loader, and then re-runs the loader. In the adapter,
235
* set the Cursor value to null. This removes the reference to the Cursor, allowing it to be
236
* garbage-collected.
237
*/
238
@Override
239
public void onLoaderReset(Loader<Cursor> cursorLoader) {
240
mAdapter.changeCursor(null);
241
}
242
243
/**
244
* Create the ActionBar.
245
*/
246
@Override
247
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
248
super.onCreateOptionsMenu(menu, inflater);
249
mOptionsMenu = menu;
250
inflater.inflate(R.menu.main, menu);
251
}
252
253
/**
254
* Respond to user gestures on the ActionBar.
255
*/
256
@Override
257
public boolean onOptionsItemSelected(MenuItem item) {
258
switch (item.getItemId()) {
259
// If the user clicks the "Refresh" button.
260
case R.id.menu_refresh:
261
SyncUtils.TriggerRefresh();
262
return true;
263
}
264
return super.onOptionsItemSelected(item);
265
}
266
267
/**
268
* Load an article in the default browser when selected by the user.
269
*/
270
@Override
271
public void onListItemClick(ListView listView, View view, int position, long id) {
272
super.onListItemClick(listView, view, position, id);
273
274
// Get a URI for the selected item, then start an Activity that displays the URI. Any
275
// Activity that filters for ACTION_VIEW and a URI can accept this. In most cases, this will
276
// be a browser.
277
278
// Get the item at the selected position, in the form of a Cursor.
279
Cursor c = (Cursor) mAdapter.getItem(position);
280
// Get the link to the article represented by the item.
281
String articleUrlString = c.getString(COLUMN_URL_STRING);
282
if (articleUrlString == null) {
283
Log.e(TAG, "Attempt to launch entry with null link");
284
return;
285
}
286
287
Log.i(TAG, "Opening URL: " + articleUrlString);
288
// Get a Uri object for the URL string
289
Uri articleURL = Uri.parse(articleUrlString);
290
Intent i = new Intent(Intent.ACTION_VIEW, articleURL);
291
startActivity(i);
292
}
293
294
/**
295
* Set the state of the Refresh button. If a sync is active, turn on the ProgressBar widget.
296
* Otherwise, turn it off.
297
*
298
* @param refreshing True if an active sync is occuring, false otherwise
299
*/
300
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
301
public void setRefreshActionButtonState(boolean refreshing) {
302
if (mOptionsMenu == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
303
return;
304
}
305
306
final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh);
307
if (refreshItem != null) {
308
if (refreshing) {
309
refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);
310
} else {
311
refreshItem.setActionView(null);
312
}
313
}
314
}
315
316
/**
317
* Crfate a new anonymous SyncStatusObserver. It's attached to the app's ContentResolver in
318
* onResume(), and removed in onPause(). If status changes, it sets the state of the Refresh
319
* button. If a sync is active or pending, the Refresh button is replaced by an indeterminate
320
* ProgressBar; otherwise, the button itself is displayed.
321
*/
322
private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
323
/** Callback invoked with the sync adapter status changes. */
324
@Override
325
public void onStatusChanged(int which) {
326
getActivity().runOnUiThread(new Runnable() {
327
/**
328
* The SyncAdapter runs on a background thread. To update the UI, onStatusChanged()
329
* runs on the UI thread.
330
*/
331
@Override
332
public void run() {
333
// Create a handle to the account that was created by
334
// SyncService.CreateSyncAccount(). This will be used to query the system to
335
// see how the sync status has changed.
336
Account account = GenericAccountService.GetAccount(SyncUtils.ACCOUNT_TYPE);
337
if (account == null) {
338
// GetAccount() returned an invalid value. This shouldn't happen, but
339
// we'll set the status to "not refreshing".
340
setRefreshActionButtonState(false);
341
return;
342
}
343
344
// Test the ContentResolver to see if the sync adapter is active or pending.
345
// Set the state of the refresh button accordingly.
346
boolean syncActive = ContentResolver.isSyncActive(
347
account, FeedContract.CONTENT_AUTHORITY);
348
boolean syncPending = ContentResolver.isSyncPending(
349
account, FeedContract.CONTENT_AUTHORITY);
350
setRefreshActionButtonState(syncActive || syncPending);
351
}
352
});
353
}
354
};
355
356
}