Android provides sync adapter framework which enables apps to create components to sync data between web server and device in the back ground. Android system uses other android system components like android accounts, bound services and content providers to provide data transfer framework.
While it is possible and easy to design and create data sync components without using android sync adapter framework, using sync adapter framework provides certain features which take time and resources to develop on your own.
Depending on the type of data and how it is protected, servers which provide data may require clients to authenticate themselves before they can access data. Android sync adapter framework uses android accounts framework to simplify capturing credentials and authenticating user in order to access server data.
Data transfer tasks can be scheduled to run automatically at scheduled times or in response to events. Sync adapter framework runs data transfer component only when there is a network connection on device.
Using sync adapter framework for data transfer reduces battery usage because of centralized management of data transfer jobs as jobs from all apps can be run at the same time.
To utilize sync adapter framework, you need to provide sync adapter, authenticator and content provider components. If you don’t use login, you have to provide stub authenticator. Similarly, if you don’t save data in content provider, you must provide stub content provider.
The component that contains data transfer code is called sync adapter component. Sync adapter component consists of sync adapter class, bound service and sync adapter xml file.
Sync adapter class is created by extending AbstractThreadedSyncAdapter class and implementing onPerformSync() method. Bound service provides interface for sync adapter framework to interact with sync adapter class. Sync adapter xml file provides information about sync adapter related to loading and scheduling.
Sync adapter framework expects authenticator component as it assumes that login is required to access server data. If your app does not use login, you can just provide dummy authenticator component.
Authenticator component works with android accounts framework. Authenticator component consists of authenticator class created by extending and implementing AbstractAccountAuthenticator abstract class, bound service used to interact with authenticator class, accounts metadata file and configuration in manifest.xml file.
To know more about android accounts, you can read my previous post android accounts, account manager and custom account type.
Sync adapter framework expects content provider. Content provider is used to store and share data on the device. If you don’t want to store data in content provide and store it in other format, you need to provide dummy content provider.
Content provider class is created by extending and implementing ContentProvider class.
To know more about content providers, read my previous post content provider.
Sync adapter can be run at a certain time each day or in regular intervals. For example, to run sync adapter after some time has elapsed, you need call ContentResolver.addPeriodicSync() method passing interval and other parameters. Method addPeriodicSync() can be called from main activity. Calling addPeriodicSync() multiple times won’t create multiple periodic sync request. Only one periodic sync request is scheduled for the account, authority and extras combination.
Sync adapter can also be run on demand in response to UI events by calling ContentResolver.requestSync().In the similar way, sync adapter can be run in response to data changes on the device and on server by calling ContentResolver.requestSync().
I’ll show how to create data transfer component using sync adapter framework by taking cashback app example. In this example, data transfer component downloads and processes latest cashback data feed from server and adds, updates and deletes data in content provider on the device.
And also example shows how to run sync adapter by using activity which runs sync adapter component periodically and on demand.
package com.zoftino.content;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class CashbackSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String CASHBACK_TABLE = "cashback_t";
public static final Uri AUTHORITY_URI = Uri.parse("content://com.zoftino.sync.cashback");
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, CASHBACK_TABLE);
private static final String CASHBACK_URL = "http://testcashback.cashback/";
private final ContentResolver mContentResolver;
private static final String[] PROJECTION = new String[]{
"_id",
"STORE",
"CASHBACK"
};
public CashbackSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContentResolver = context.getContentResolver();
}
public CashbackSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
mContentResolver = context.getContentResolver();
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
try {
final URL location = new URL(CASHBACK_URL);
InputStream stream = null;
try {
Log.i("cashback sync adaper", "onPerformSync running");
// stream = downloadUrl(location);
// String feedData = readInput(stream);
//syn adapter can be run without downloading using test data
String feedData = getTestData();
Map<Long, CashbackEntity> cashbackFeed = processCashbackFeed(feedData);
addUpdateDeleteLocalData(cashbackFeed, syncResult);
} finally {
if (stream != null) {
stream.close();
}
}
} catch (Exception e) {
e.printStackTrace();
return;
}
}
public void addUpdateDeleteLocalData(Map<Long, CashbackEntity> cashbackData, final SyncResult syncResult)
throws Exception {
final ContentResolver contentResolver = getContext().getContentResolver();
ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();
Cursor c = contentResolver.query(CONTENT_URI, PROJECTION, null, null, null);
long id;
String store;
String cashback;
if (c != null) {
while (c.moveToNext()) {
syncResult.stats.numEntries++;
id = c.getLong(0);
store = c.getString(1);
cashback = c.getString(2);
CashbackEntity cashbackExist = cashbackData.get(id);
if (cashbackExist != null) {
cashbackData.remove(id);
Uri existingUri = CONTENT_URI.buildUpon()
.appendPath(Long.toString(id)).build();
//update record as local data is different
if ((cashbackExist.getStore() != null && !cashbackExist.getStore().equals(store)) ||
(cashbackExist.getCashback() != null && !cashbackExist.getCashback().equals(cashback))) {
batch.add(ContentProviderOperation.newUpdate(existingUri)
.withValue("STORE", cashbackExist.getStore())
.withValue("CASHBACK", cashbackExist.getCashback())
.build());
syncResult.stats.numUpdates++;
}
} else {
//delete local record as it does not exist in server
Uri deleteUri = CONTENT_URI.buildUpon()
.appendPath(Long.toString(id)).build();
batch.add(ContentProviderOperation.newDelete(deleteUri).build());
syncResult.stats.numDeletes++;
}
}
c.close();
}
// Add New records
for (CashbackEntity ce : cashbackData.values()) {
batch.add(ContentProviderOperation.newInsert(CONTENT_URI)
.withValue("_id", ce.id)
.withValue("STORE", ce.getStore())
.withValue("CASHBACK", ce.getCashback())
.build());
syncResult.stats.numInserts++;
}
mContentResolver.applyBatch("com.zoftino.sync.cashback", batch);
}
private Map<Long, CashbackEntity> processCashbackFeed(String feed) {
Map<Long, CashbackEntity> cashbackFeed = new HashMap<Long, CashbackEntity>();
String[] cbrecs = feed.split(",");
for (String cbrec : cbrecs) {
String[] cbdata = cbrec.split("\\|");
CashbackEntity cbe = new CashbackEntity();
cbe.setId(Long.valueOf(cbdata[0]));
cbe.setStore(cbdata[1]);
cbe.setCashback(cbdata[2]);
cashbackFeed.put(cbe.getId(), cbe);
}
Log.i("cashback sync adaper", "total recs in map " + cashbackFeed.keySet().size());
return cashbackFeed;
}
private InputStream downloadUrl(final URL url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(20000);
conn.setConnectTimeout(15000);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
return conn.getInputStream();
}
private String readInput(InputStream is) {
String str = "";
StringBuffer buf = new StringBuffer();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
if (is != null) {
while ((str = reader.readLine()) != null) {
buf.append(str + ",");
}
}
} catch (Exception e) {
} finally {
try {
is.close();
} catch (Throwable ignore) {
}
}
return buf.toString();
}
private String getTestData() {
String cashbackFedd = "1|fashion store|Upto 2% cashback,";
cashbackFedd = cashbackFedd + "2|shoes store|Upto 21% cashback,";
cashbackFedd = cashbackFedd + "3|electronics store|Upto 12% cashback,";
cashbackFedd = cashbackFedd + "4|travel store|Upto 24% cashback,";
cashbackFedd = cashbackFedd + "6|mobiles store|Upto 32% cashback,";
cashbackFedd = cashbackFedd + "7|blah store|Upto 52% cashback,";
cashbackFedd = cashbackFedd + "8|xtz store|Upto 23% cashback";
cashbackFedd = cashbackFedd + "9|xyz store|Upto 23% cashback";
return cashbackFedd;
}
}
package com.zoftino.content;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class CashbackSyncAdapterService extends Service {
private static final Object sSyncAdapterLock = new Object();
private static CashbackSyncAdapter sSyncAdapter = null;
@Override
public void onCreate() {
super.onCreate();
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new CashbackSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.zoftino.sync.cashback"
android:accountType="com.zoftino.sync"
android:userVisible="false"
android:supportsUploading="false"
android:allowParallelSyncs="false"
android:isAlwaysSyncable="true"></sync-adapter>
package com.zoftino.content;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.Nullable;
public class CashbackContentProvider extends ContentProvider {
private CashbackSQLiteOpenHelper sqLiteOpenHelper;
private static final String CASHBACK_DBNAME = "zoftino_cashback";
private static final String CASHBACK_TABLE = "cashback_t";
private SQLiteDatabase cbDB;
private static final String SQL_CREATE_CASHBACK = "CREATE TABLE " +
CASHBACK_TABLE +
"(" +
"_id INTEGER PRIMARY KEY, " +
"STORE TEXT, " +
"CASHBACK TEXT)";
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI("com.zoftino.sync.cashback", CASHBACK_TABLE, 1);
}
@Override
public boolean onCreate() {
sqLiteOpenHelper = new CashbackSQLiteOpenHelper( getContext(), CASHBACK_DBNAME, SQL_CREATE_CASHBACK);
return true;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
String tableNme = "";
switch(uriMatcher.match(uri)){
case 1 :
tableNme = CASHBACK_TABLE;
break;
default:
return null;
}
cbDB = sqLiteOpenHelper.getWritableDatabase();
Cursor cursor = (SQLiteCursor) cbDB.query(tableNme, projection, selection, selectionArgs,
null, null, sortOrder);
return cursor;
}
@Nullable
@Override
public String getType(Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
try {
String tableNme = "";
switch (uriMatcher.match(uri)) {
case 1:
tableNme = CASHBACK_TABLE;
break;
default:
return null;
}
cbDB = sqLiteOpenHelper.getWritableDatabase();
long rowid = cbDB.insert(tableNme, null, contentValues);
return getContentUriRow(rowid);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
@Override
public int delete(Uri uri, String where, String[] selectionArgs) {
String tableNme = CASHBACK_TABLE;
cbDB = sqLiteOpenHelper.getWritableDatabase();
return cbDB.delete(tableNme, where, selectionArgs);
}
@Override
public int update(Uri uri, ContentValues contentValues, String where, String[] selectionArgs) {
String tableNme = CASHBACK_TABLE;
cbDB = sqLiteOpenHelper.getWritableDatabase();
return cbDB.update(tableNme,contentValues,where,selectionArgs );
}
private Uri getContentUriRow(long rowid){
return Uri.fromParts("com.zoftino.sync.cashback", CASHBACK_TABLE, Long.toString(rowid));
}
public class CashbackSQLiteOpenHelper extends SQLiteOpenHelper {
private String sql;
CashbackSQLiteOpenHelper(Context context, String dbName, String msql) {
super(context, dbName, null, 1);
sql = msql;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { }
}
}
package com.zoftino.content;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.os.Bundle;
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
public class CashbackAuthenticator extends AbstractAccountAuthenticator {
private Context context;
public CashbackAuthenticator(Context ctx){
super(ctx);
context = ctx;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, String s) {
return null;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String s) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String[] strings) throws NetworkErrorException {
final Bundle result = new Bundle();
result.putBoolean(KEY_BOOLEAN_RESULT, false);
return result;
}
}
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.zoftino.sync"
android:icon="@drawable/zoftino"
android:smallIcon="@drawable/zoftino"
android:label="@string/app_name"></account-authenticator>
package com.zoftino.content;
import android.accounts.Account;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class CashbackAccountService extends Service {
private static final String ACCOUNT_TYPE = "com.zoftino.sync";
public static final String ACCOUNT_NAME = "zoftino_sync";
private CashbackAuthenticator mAuthenticator;
public static Account GetAccount() {
final String accountName = ACCOUNT_NAME;
return new Account(accountName, ACCOUNT_TYPE);
}
@Override
public void onCreate() {
mAuthenticator = new CashbackAuthenticator(this);
}
@Override
public void onDestroy() {
}
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zoftino.content">
<uses-permission android:name="android.permission.INTERNET"></<uses-permission>
<uses-permission android:name="android.permission.READ_SYNC_STATS"></<uses-permission>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"></<uses-permission>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"></<uses-permission>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".CashbackSyncAdapterRunActivity"></activity>
<provider
android:name=".CashbackContentProvider"
android:authorities="com.zoftino.sync.cashback"
android:exported="false"
android:syncable="true"></provider>
<service android:name=".CashbackAccountService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"></action>
</intent-filter>
<meta-data android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/cashback_authenticator"></meta-data>
</service>
<service android:name=".CashbackSyncAdapterService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter"></action>
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/cashback_syncadapter"></meta-data>
</service>
</application>
</manifest>
package com.zoftino.content;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
public class CashbackSyncAdapterRunActivity extends AppCompatActivity {
public static final String AUTHORITY = "com.zoftino.sync.cashback";
public static final String ACCOUNT_TYPE = "com.zoftino.sync";
public static final String ACCOUNT = "cashbacksync";
public static final int SYNC_INTERVAL = 25000;
private Account mAccount;
private ContentResolver mContentResolver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_syncadapter_run);
mAccount = CreateSyncAccount(this);
mContentResolver = getContentResolver();
ContentResolver.addPeriodicSync(
mAccount,
AUTHORITY,
Bundle.EMPTY,
SYNC_INTERVAL);
}
public void runSyncAdapter(View v) {
Bundle bundle = new Bundle();
bundle.putBoolean(
ContentResolver.SYNC_EXTRAS_MANUAL, true);
bundle.putBoolean(
ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(mAccount, AUTHORITY, bundle);
}
public Account CreateSyncAccount(Context context) {
Account newAccount = new Account(ACCOUNT, ACCOUNT_TYPE);
AccountManager accountManager =
(AccountManager) context.getSystemService(ACCOUNT_SERVICE);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.GET_ACCOUNTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.GET_ACCOUNTS}, 31);
}
Account accounts[] = accountManager.getAccountsByType(ACCOUNT_TYPE);
if (accounts == null || accounts.length < 1) {
if (accountManager.addAccountExplicitly(newAccount, null, null)) {
return newAccount;
} else {
Log.i("sync activity", "error creating account");
}
} else {
return accounts[0];
}
return null;
}
}