📜 ⬆️ ⬇️

Synchronization in Android applications. Part two

account
Colleagues, good afternoon. Let's continue the topic begun in the last article , where we discussed the mechanism for creating an account on the device. This was the first prerequisite for using the SyncAdapter Framework.

The second condition is the presence of ContentProvider'a , the process of writing which is chewed in the documentation. Frankly, I don’t really like the way it is described there: everything seems cumbersome and complicated. Therefore, a little bicycles and even once more live through this topic. It would be possible to get along with the stub-provider, but we are serious people and we will use the full power of this tool.

In the comments to the previous part flashed, please consider the case when we do not need authorization, but only synchronization. Such a case and consider. As an example, we take and write a simple rss reader to read our favorite Habr and not only. Yes, that's so trite.
')
The application will be able to add / delete tapes, view the list of news and open them in the browser. We will visualize the synchronization process and its launch using the SwipeRefreshLayout class recently added to the support-library. You can read what it is and how to use it here .

To set up automatic synchronization at regular intervals, we need a settings screen for this good. It is desirable that access to it was not only from the application, but also from the system screen of our account (as in the screenshot for the article). Use for this PreferenceFragment'y . Determined the functionality, let's start.

Account


How to add an account to the application you already know from the previous part. But for our application, we do not need authorization; accordingly, we replace the Authenticator with an empty implementation.
Authenticator.java
public class Authenticator extends AbstractAccountAuthenticator { public Authenticator(Context context) { super(context); } @Override public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { throw new UnsupportedOperationException(); } @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public String getAuthTokenLabel(String authTokenType) { throw new UnsupportedOperationException(); } @Override public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { throw new UnsupportedOperationException(); } } 


We will need to slightly modify the res / xml / authenticator.xml file to add the ability to go to the synchronization settings screen. Add the parameter android: accountPreferences with an indication of the file from which these same Preferences need to be pulled up. When you click on the “Sync” item, the SyncSettingsActivity of our application will open.
authenticator.xml
 <?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountPreferences="@xml/account_prefs" android:accountType="com.elegion.newsfeed.account" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:smallIcon="@drawable/ic_launcher" /> 


account_prefs.xml
 <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:persistent="true"> <PreferenceCategory android:title="@string/general_settings" /> <PreferenceScreen android:key="com.elegion.newsfeed.KEY_ACCOUNT_SYNC" android:summary="@string/sync_settings_summary" android:title="@string/sync"> <intent android:action="com.elegion.newsfeed.ACTION_SYNC_SETTINGS" android:targetClass="com.elegion.newsfeed.activity.SyncSettingsActivity" android:targetPackage="com.elegion.newsfeed" /> </PreferenceScreen> </PreferenceScreen> 


ContentProvider


Our provider will be a wrapper over a SQLite database in which we will store the news. Let us dwell a bit and take a closer look at its implementation. The provider can work with two types of Uri:
content: // authority / table - select all values ​​from a table
content: // authority / table / _id - fetching one value by primary key
in the onCreate method using PackageManager.getProviderInfo we get the authority for this provider and register them with SQLiteUriMatcher. What happens in the methods: the provider takes the name of the table from uri, then the specific SQLiteTableProvider implementation (the provider for the table) is taken from SCHEMA for this table. In SQLiteTableProvider, the corresponding methods are called (in fact, the call is proxied). This approach allows for each table to customize work with data. Depending on the results, the ContentResolver (and with it our application) receives a notification of data changes. For content: // authority / table / _id uri, the where clause is rewritten to ensure that the primary key works. If you wish, you can screw this provider up a bit and take it to the library class. As practice shows, such an implementation is sufficient for 90% of the tasks (the remaining 10 are full text search, like nocase search).
SQLiteContentProvider.java
 public class SQLiteContentProvider extends ContentProvider { private static final String DATABASE_NAME = "newsfeed.db"; private static final int DATABASE_VERSION = 1; private static final String MIME_DIR = "vnd.android.cursor.dir/"; private static final String MIME_ITEM = "vnd.android.cursor.item/"; private static final Map<String, SQLiteTableProvider> SCHEMA = new ConcurrentHashMap<>(); static { SCHEMA.put(FeedProvider.TABLE_NAME, new FeedProvider()); SCHEMA.put(NewsProvider.TABLE_NAME, new NewsProvider()); } private final SQLiteUriMatcher mUriMatcher = new SQLiteUriMatcher(); private SQLiteOpenHelper mHelper; private static ProviderInfo getProviderInfo(Context context, Class<? extends ContentProvider> provider, int flags) throws PackageManager.NameNotFoundException { return context.getPackageManager() .getProviderInfo(new ComponentName(context.getPackageName(), provider.getName()), flags); } private static String getTableName(Uri uri) { return uri.getPathSegments().get(0); } @Override public boolean onCreate() { try { final ProviderInfo pi = getProviderInfo(getContext(), getClass(), 0); final String[] authorities = TextUtils.split(pi.authority, ";"); for (final String authority : authorities) { mUriMatcher.addAuthority(authority); } mHelper = new SQLiteOpenHelperImpl(getContext()); return true; } catch (PackageManager.NameNotFoundException e) { throw new SQLiteException(e.getMessage()); } } @Override public Cursor query(Uri uri, String[] columns, String where, String[] whereArgs, String orderBy) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } final Cursor cursor = tableProvider.query(mHelper.getReadableDatabase(), columns, where, whereArgs, orderBy); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @Override public String getType(Uri uri) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } else if (matchResult == SQLiteUriMatcher.MATCH_ID) { return MIME_ITEM + getTableName(uri); } return MIME_DIR + getTableName(uri); } @Override public Uri insert(Uri uri, ContentValues values) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { final int affectedRows = updateInternal( tableProvider.getBaseUri(), tableProvider, values, BaseColumns._ID + "=?", new String[]{uri.getLastPathSegment()} ); if (affectedRows > 0) { return uri; } } final long lastId = tableProvider.insert(mHelper.getWritableDatabase(), values); getContext().getContentResolver().notifyChange(tableProvider.getBaseUri(), null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_LAST_ID, lastId); tableProvider.onContentChanged(getContext(), SQLiteOperation.INSERT, extras); return uri; } @Override public int delete(Uri uri, String where, String[] whereArgs) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } final int affectedRows = tableProvider.delete(mHelper.getWritableDatabase(), where, whereArgs); if (affectedRows > 0) { getContext().getContentResolver().notifyChange(uri, null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_AFFECTED_ROWS, affectedRows); tableProvider.onContentChanged(getContext(), SQLiteOperation.DELETE, extras); } return affectedRows; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } return updateInternal(tableProvider.getBaseUri(), tableProvider, values, where, whereArgs); } private int updateInternal(Uri uri, SQLiteTableProvider provider, ContentValues values, String where, String[] whereArgs) { final int affectedRows = provider.update(mHelper.getWritableDatabase(), values, where, whereArgs); if (affectedRows > 0) { getContext().getContentResolver().notifyChange(uri, null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_AFFECTED_ROWS, affectedRows); provider.onContentChanged(getContext(), SQLiteOperation.UPDATE, extras); } return affectedRows; } private static final class SQLiteOpenHelperImpl extends SQLiteOpenHelper { public SQLiteOpenHelperImpl(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.beginTransactionNonExclusive(); try { for (final SQLiteTableProvider table : SCHEMA.values()) { table.onCreate(db); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.beginTransactionNonExclusive(); try { for (final SQLiteTableProvider table : SCHEMA.values()) { table.onUpgrade(db, oldVersion, newVersion); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } } 


Now you need to register the provider in AndroidManifest.xml and pay attention to the android parameter : syncable = "true" . This flag indicates that our provider supports synchronization.
AndroidManifest.xml
 <provider android:name=".sqlite.SQLiteContentProvider" android:authorities="com.elegion.newsfeed" android:exported="false" android:syncable="true" /> 


Also of interest is the FeedProvider class - an implementation of SQLiteTableProvider for working with news feeds. When inserting (!) Into this table (subscription to a new tape) forced synchronization will be called. The onContentChanged method, which is pulled from the SQLiteContentProvider when the data changes (insert / update / delete), is responsible for this. A trigger will be created for the table ( onCreate ), which will delete news related to the feed. Why it is worth calling synchronization only when pasting? To avoid looping, because our provider will update the table (add a caption, a link to a picture, a publication date, etc.). Additional synchronization parameters are transmitted via syncExtras.
FeedProvider.java
 public class FeedProvider extends SQLiteTableProvider { public static final String TABLE_NAME = "feeds"; public static final Uri URI = Uri.parse("content://com.elegion.newsfeed/" + TABLE_NAME); public FeedProvider() { super(TABLE_NAME); } public static long getId(Cursor c) { return c.getLong(c.getColumnIndex(Columns._ID)); } public static String getIconUrl(Cursor c) { return c.getString(c.getColumnIndex(Columns.IMAGE_URL)); } public static String getTitle(Cursor c) { return c.getString(c.getColumnIndex(Columns.TITLE)); } public static String getLink(Cursor c) { return c.getString(c.getColumnIndex(Columns.LINK)); } public static long getPubDate(Cursor c) { return c.getLong(c.getColumnIndex(Columns.PUB_DATE)); } public static String getRssLink(Cursor c) { return c.getString(c.getColumnIndex(Columns.RSS_LINK)); } @Override public Uri getBaseUri() { return URI; } @Override public void onContentChanged(Context context, int operation, Bundle extras) { if (operation == INSERT) { extras.keySet(); final Bundle syncExtras = new Bundle(); syncExtras.putLong(SyncAdapter.KEY_FEED_ID, extras.getLong(KEY_LAST_ID, -1)); ContentResolver.requestSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, syncExtras); } } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table if not exists " + TABLE_NAME + "(" + Columns._ID + " integer primary key on conflict replace, " + Columns.TITLE + " text, " + Columns.LINK + " text, " + Columns.IMAGE_URL + " text, " + Columns.LANGUAGE + " text, " + Columns.PUB_DATE + " integer, " + Columns.RSS_LINK + " text unique on conflict ignore)"); db.execSQL("create trigger if not exists after delete on " + TABLE_NAME + " begin " + " delete from " + NewsProvider.TABLE_NAME + " where " + NewsProvider.Columns.FEED_ID + "=old." + Columns._ID + ";" + " end;"); } public interface Columns extends BaseColumns { String TITLE = "title"; String LINK = "link"; String IMAGE_URL = "imageUrl"; String LANGUAGE = "language"; String PUB_DATE = "pubDate"; String RSS_LINK = "rssLink"; } } 


Behind this, the rabbit mink ends and the looking-glass begins.

Syncadapter


Before breaking into the process of creating a SyncAdapter, let's think about why it is needed at all, what advantages it gives. If you believe the documentation, then at least we get:


Already not bad, right? We add that when using ContentProvider, we can start synchronization when data changes in it. This completely removes the need for us to track changes in the data in the application and perform synchronization in “manual mode”.

The process of integrating this good is very similar to the process of integrating your account into an application. We will need an implementation of AbstractThreadedSyncAdapter and Service for integration into the system. AbstractThreadedSyncAdapter has just one abstract onPerformSync method, in which all the magic happens. What exactly is going on here? Depending on the extras-parameters passed (remember syncExtras in FeedProvider.onContentChanged), either one tape or all is synchronized. In general, we select tapes from the database, parse rss by reference and add them to our database using the ContentProviderClient provider . To inform the system about the status (number of updates, errors, etc.) synchronization is used SyncResult syncResult .
SyncAdapter.java
 public class SyncAdapter extends AbstractThreadedSyncAdapter { public static final String KEY_FEED_ID = "com.elegion.newsfeed.sync.KEY_FEED_ID"; public SyncAdapter(Context context) { super(context, true); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { final long feedId = extras.getLong(KEY_FEED_ID, -1); if (feedId > 0) { syncFeeds(provider, syncResult, FeedProvider.Columns._ID + "=?", new String[]{String.valueOf(feedId)}); } else { syncFeeds(provider, syncResult, null, null); } } private void syncFeeds(ContentProviderClient provider, SyncResult syncResult, String where, String[] whereArgs) { try { final Cursor feeds = provider.query( FeedProvider.URI, new String[]{ FeedProvider.Columns._ID, FeedProvider.Columns.RSS_LINK }, where, whereArgs, null ); try { if (feeds.moveToFirst()) { do { syncFeed(feeds.getString(0), feeds.getString(1), provider, syncResult); } while (feeds.moveToNext()); } } finally { feeds.close(); } } catch (RemoteException e) { Log.e(SyncAdapter.class.getName(), e.getMessage(), e); ++syncResult.stats.numIoExceptions; } } private void syncFeed(String feedId, String feedUrl, ContentProviderClient provider, SyncResult syncResult) { try { final HttpURLConnection cn = (HttpURLConnection) new URL(feedUrl).openConnection(); try { final RssFeedParser parser = new RssFeedParser(cn.getInputStream()); try { parser.parse(feedId, provider, syncResult); } finally { parser.close(); } } finally { cn.disconnect(); } } catch (IOException e) { Log.e(SyncAdapter.class.getName(), e.getMessage(), e); ++syncResult.stats.numIoExceptions; } } } 


The implementation of SyncService is also very simple. All we need is to give the IBinder object to the system, to communicate with our SyncAdapter. In order for the system to understand what adapter we are registering, you need the xml-meta file sync_adapter.xml, and also register all this stuff in AndroidManifest.xml.
SyncService.java
 public class SyncService extends Service { private static SyncAdapter sSyncAdapter; @Override public void onCreate() { super.onCreate(); if (sSyncAdapter == null) { sSyncAdapter = new SyncAdapter(getApplicationContext()); } } @Override public IBinder onBind(Intent intent) { return sSyncAdapter.getSyncAdapterBinder(); } } 


sync_adapter.xml
 <?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="com.elegion.newsfeed.account" android:allowParallelSyncs="false" android:contentAuthority="com.elegion.newsfeed" android:isAlwaysSyncable="true" android:supportsUploading="false" android:userVisible="true" /> 


AndroidManifest.xml
 <service android:name=".sync.SyncService" android:exported="false" android:process=":sync"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_adapter" /> </service> 


And now an example


image
This is how the window with the list of ribbons will look. As you remember, we agreed to use SwipeRefreshLayout to force synchronization and visualization of this process. FeedList.java feed list and NewsList.java news list will be inherited from common parent SwipeToRefreshList.java .

To track the synchronization status, you must register the Observer in the ContentResolver (the SwipeToRefreshList.onResume () method). To do this, use the ContentResolver.addStatusChangeListener method. In the SwipeToRefreshList.onStatusChanged method, we check the synchronization status using the ContentResolver.isSyncActive method and pass this result to the SwipeToRefreshList.onSyncStatusChanged method, which will be redefined by successors. All that this method will do is hide / show a progress bar for SwipeRefreshLayout . Since SyncStatusObserver.onStatusChanged is called from a separate thread, we wrap the result in a handler. The SwipeToRefreshList.onRefresh method in descendants starts forced synchronization using ContentResolver.requestSync .

All lists are loaded and displayed using the CursorLoader + CursorAdapter , which also work wonderfully in conjunction with the ContentProvider, eliminating the need to keep track of the relevance of the lists. As soon as a new item is added to the provider, all CursorLoaders will receive notifications and update the data in the CursorAdapters.
SwipeToRefreshList.java
 public class SwipeToRefreshList extends Fragment implements SwipeRefreshLayout.OnRefreshListener, SyncStatusObserver, AdapterView.OnItemClickListener, SwipeToDismissCallback { private SwipeRefreshLayout mRefresher; private ListView mListView; private Object mSyncMonitor; private SwipeToDismissController mSwipeToDismissController; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fmt_swipe_to_refresh_list, container, false); mListView = (ListView) view.findViewById(android.R.id.list); return (mRefresher = (SwipeRefreshLayout) view); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mRefresher.setColorScheme( android.R.color.holo_blue_light, android.R.color.holo_red_light, android.R.color.holo_green_light, android.R.color.holo_orange_light ); mSwipeToDismissController = new SwipeToDismissController(mListView, this); } @Override public void onResume() { super.onResume(); mRefresher.setOnRefreshListener(this); mSyncMonitor = ContentResolver.addStatusChangeListener( ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, this ); mListView.setOnItemClickListener(this); mListView.setOnTouchListener(mSwipeToDismissController); mListView.setOnScrollListener(mSwipeToDismissController); } @Override public void onPause() { mRefresher.setOnRefreshListener(null); ContentResolver.removeStatusChangeListener(mSyncMonitor); mListView.setOnItemClickListener(null); mListView.setOnTouchListener(null); mListView.setOnScrollListener(null); super.onPause(); } @Override public final void onRefresh() { onRefresh(AppDelegate.sAccount); } @Override public final void onStatusChanged(int which) { mRefresher.post(new Runnable() { @Override public void run() { onSyncStatusChanged(AppDelegate.sAccount, ContentResolver .isSyncActive(AppDelegate.sAccount, AppDelegate.AUTHORITY)); } }); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { } @Override public boolean canDismissView(View view, int position) { return false; } @Override public void dismissView(View view, int position) { } public void setListAdapter(ListAdapter adapter) { final DataSetObserver dataSetObserver = mSwipeToDismissController.getDataSetObserver(); final ListAdapter oldAdapter = mListView.getAdapter(); if (oldAdapter != null) { oldAdapter.unregisterDataSetObserver(dataSetObserver); } mListView.setAdapter(adapter); adapter.registerDataSetObserver(dataSetObserver); } protected void onRefresh(Account account) { } protected void onSyncStatusChanged(Account account, boolean isSyncActive) { } protected void setRefreshing(boolean refreshing) { mRefresher.setRefreshing(refreshing); } } 


image
So, with forced synchronization sorted out. But the juice is automatic synchronization. Remember, we added support for the settings screen to our account? Good practice is not to force the user to perform unnecessary actions. Therefore, access to this screen is duplicated by a button in the action bar.

What he is like is seen on the left. Technically, this is an activation with one PreferenceFragment ( SyncSettings.java ), whose settings are taken from res / xml / sync_prefs.xml .

Change of parameters is traced in the onSharedPreferenceChanged method (implementation of OnSharedPreferenceChangeListener ). To enable periodic synchronization, there is the ContentResolver.addPeriodicSync method, but strangely enough, the ContentResolver.removePeriodicSync method for disabling. To update the synchronization interval, use the ContentResolver.addPeriodicSync method as well . Because, according to the documentation for this method: "If you have the answer, you’ve been updated." if synchronization is already scheduled, extra and authority will not be added to the new synchronization, instead the previous one will be updated).



sync_prefs.xml
 <?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:key="com.elegion.newsfeed.KEY_SYNC" android:title="@string/sync"> <CheckBoxPreference android:defaultValue="false" android:key="com.elegion.newsfeed.KEY_AUTO_SYNC" android:summary="@string/auto_sync_summary" android:title="@string/auto_sync" /> <ListPreference android:defaultValue="@string/auto_sync_interval_default" android:dependency="com.elegion.newsfeed.KEY_AUTO_SYNC" android:entries="@array/auto_sync_intervals" android:entryValues="@array/auto_sync_interval_values" android:key="com.elegion.newsfeed.KEY_AUTO_SYNC_INTERVAL" android:title="@string/auto_sync_interval" /> </PreferenceCategory> </PreferenceScreen> 


SyncSettings.java
 public class SyncSettings extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String KEY_AUTO_SYNC = "com.elegion.newsfeed.KEY_AUTO_SYNC"; private static final String KEY_AUTO_SYNC_INTERVAL = "com.elegion.newsfeed.KEY_AUTO_SYNC_INTERVAL"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.sync_prefs); final ListPreference interval = (ListPreference) getPreferenceManager() .findPreference(KEY_AUTO_SYNC_INTERVAL); interval.setSummary(interval.getEntry()); } @Override public void onResume() { super.onResume(); getPreferenceManager().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { getPreferenceManager().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (TextUtils.equals(KEY_AUTO_SYNC, key)) { if (prefs.getBoolean(key, false)) { final long interval = Long.parseLong(prefs.getString( KEY_AUTO_SYNC_INTERVAL, getString(R.string.auto_sync_interval_default) )); ContentResolver.addPeriodicSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, Bundle.EMPTY, interval); } else { ContentResolver.removePeriodicSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, new Bundle()); } } else if (TextUtils.equals(KEY_AUTO_SYNC_INTERVAL, key)) { final ListPreference interval = (ListPreference) getPreferenceManager().findPreference(key); interval.setSummary(interval.getEntry()); ContentResolver.addPeriodicSync( AppDelegate.sAccount, AppDelegate.AUTHORITY, Bundle.EMPTY, Long.parseLong(interval.getValue()) ); } } } 


Having collected all this in a bunch, we get a working application, with all the buns that the Android system provides us. Behind the scenes, a lot of delicious things remained, but this is enough to understand the power of the SyncAdapter Framework.

That seems to be all. Full source code of the project can be found here . Thank you for attention. Constructive criticism is welcome.

Synchronization in Android applications. Part one.

Source: https://habr.com/ru/post/216857/


All Articles