官方文档:https://developer.android.com/training/sync-adapters/index.html
在Android设备和web服务器之间同步数据会使你的应用更实用,更吸引用户,例如,将手机数据传到服务端实现数据备份,将数据从服务端取回让用户能够脱机使用。在某些情况下,用户会发现这样会更方便:通过web修改信息然后在手机上就可以继续使用,或者隔一段时间将手机上的数据上传到一个总存储区。
虽然你可以在应用中设计自己的数据传输系统,但也应该考虑一下用Android的sync adater框架。它可以协助管理和自动发起数据传输,也可以协调不同应用的同步操作。使用这个同步框架比自己设计数据传输策略有如下优势:
- 插件架构:
可以将数据传输的代码以可调用组件的形式添加到系统中。
- 自动执行:
可以根据不同条件自动发起数据传输,比如数据变更,间隔一定时间,或者是每天定时。而且,系统会将暂时不能运行的操作添加到队列里,在可能的情况下重新发起。
- 自动检查网络:
系统只会在有网络的情况下发起数据传输
- 优化电池性能:
可以集中处理数据传输任务。将你的应用的数据传输与其他应用的传输结合,减少系统切换网络的次数,从而降低功耗。
- 账号管理认证:
如果你的应用需要用户认证功能,你可以选择在数据传输中整合进账号管理认证。
账号同步框架组成部分有三,如上。三者缺一不可。账号是入口,里面可以进行账号验证操作,当然不需要这个功能,相应方法返回false或者null即可。通过了账号认证之后,到了同步管理,里面来进行数据的同步的操作,至于数据发生冲突的具体逻辑需要你来处理。还有个StubProvider,是用来配合同步更新操作的。
下面是manifest中的注册:
对应的服务:
public class AuthenticatorService extends Service {
//mAuthenticator目的是作为账号认证
private Authenticator mAuthenticator;
public AuthenticatorService() {
}
@Override
public void onCreate() {
super.onCreate();
mAuthenticator = new Authenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
//主要起作用的mAuthenticator
return mAuthenticator.getIBinder();
}
}
Authenticator是一个继承自AbstractAccountAuthenticator的类,AbstractAccountAuthenticator是一个虚类,它定义处理手机“设置”里“账号与同步”中Account的添加、删除和验证等功能的基本接口,并实现了一些基本功能。
AbstractAccountAuthenticator里面有个继承于IAccountAuthenticator.Stub的内部类,以用来对AbstractAccountAuthenticator的远程接口调用进行包装。我们可以通过AbstractAccountAuthenticator的getIBinder()方法,返回内部类的IBinder形式,以便对此类进行远程调用,如上面代码onBind方法中的调用。
其中比较重要需要重载的方法是addAccount():
@Override
public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {
Log.d(TAG, "Authenticator addAccount : ");
Intent intent = new Intent("com.jiahui.xx.syncadapter");
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, accountAuthenticatorResponse);
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
//return null;
}
如上图,这个addAccount()在用户进入设置-账户-添加账户的时候触发的,这里面把自己设置账户的页面的信息封装给bundle,然后传出去即可。如果返回null表示不做任何触发。
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.crazyman.accountsyncdemo.type"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:smallIcon="@mipmap/ic_launcher" />
sync机制的使用和账号管理很类似,也是基于binder机制的跨进程通信。首先它需要一个Service,这个服务提供一个Action给系统以便系统能找到它;然后就是继承和实现AbstractThreadedSyncAdapter,此类中包含实现了ISyncAdapter.Stub内部类,这个内部类封装了远程接口调用,这个类getSyncAdapterBinder()方法,返回内部类的IBinder形式,以便对AbstractThreadedSyncAdapte进行远程调用;在manifest中需要对Service注册,而且指定meta-data,这个meta-data是一个xml文件,在SampleSyncAdapter实例中,它的名字是syncadapter.xml,这个文件指定了账号和被监听的contentprovider。下面分别介绍这几个文件:
public class SyncService extends Service {
private static final String TAG = "SyncService";
private static final Object sSyncAdapterLock = new Object();
private static SyncAdapter sSyncAdapter = null;
/**
* Thread-safe constructor, creates static {@link SyncAdapter} instance.
*/
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Service created");
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
@Override
/**
* Logging-only destructor.
*/
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "Service destroyed");
}
/**
* Return Binder handle for IPC communication with {@link SyncAdapter}.
*
* New sync requests will be sent directly to the SyncAdapter using this channel.
*
* @param intent Calling intent
* @return Binder handle for {@link SyncAdapter}
*/
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}
Manifest.xml注册如下:
<service
android:name=".syncadapter.SyncService"
android:exported="true">
<intent-filter>
<action
android:name="android.content.SyncAdapter" />
intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
service>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.crazyman.accountsyncdemo.type"
android:allowParallelSyncs="false"
android:contentAuthority="com.crazyman.accountsyncdemo.provider"
android:isAlwaysSyncable="true"
android:supportsUploading="false"
android:userVisible="false" />
syncadapter.xml文件指定了此Service所监听的contentprovider的Authority,还指定了监听此Authority的账号类型accountType。常用属性以及作用如下:
属性 | 描述 |
---|---|
android:contentAuthority | 指定要同步的ContentProvider所需的权限 |
android:accountType | 表示进行同步的账号的类型 |
android:userVisible | 设置是否在“设置–账户”中显示,当设置为true时,用户可手动关闭同步功能 |
android:supportsUploading | 设置是否必须notifyChange通知才能同步 |
android:allowParallelSyncs | 是否支持并发同步 |
android:isAlwaysSyncable | 默认是false,framework是否可以在任意时刻运行SyncAdapter,如果仅希望通过程序控制同步发起,则设为false,然后通过调用requestSync() 发起 |
public class SyncAdapter extends AbstractThreadedSyncAdapter {
private static final String TAG = SyncAdapter.class.getSimpleName();
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.d(TAG, "SyncAdapter onPerformSync : 同步数据开始");
// TODO: 2017/7/1 执行具体数据同步工作
Log.d(TAG, "SyncAdapter onPerformSync : 同步数据结束");
}
}
前面说了,整个框架必须有个ContentProvider作为组成部分,当然你实际也可以不使用,在syncadapter的:
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.d(TAG, "SyncAdapter onPerformSync : 同步数据开始");
// TODO: 2017/7/1 执行具体数据同步工作
Log.d(TAG, "SyncAdapter onPerformSync : name: " + account.name + " type: " + account.type);
Log.d(TAG, "SyncAdapter onPerformSync : " + authority);
Log.d(TAG, "SyncAdapter onPerformSync : 同步数据结束");
}
里面传递过来的参数provider就是配置的Contentprivder。
而且这个StubProvider需要在Manifest.xml中注册,还有需要设置属性syncable = true:
<provider
android:name="com.crazyman.accountsyncdemo.StubProvider"
android:authorities="com.crazyman.accountsyncdemo.provider"
android:exported="false"
android:syncable="true" />
如果不在Manifest.xml中设置,也可以在代码中设置
ContentResolver.setIsSyncable(account, "com.crazyman.accountsyncdemo.provider", 1);
两者一致,最后一个参数的意思是:
@param syncable >0 denotes syncable, 0 means not syncable, <0 means unknowns
同步框架有几种数据同步的情况需要处理:
1. 数据在服务端变化,-看6.1;
2. 数据在客户端变化,-看6.2;
3. 系统连接TCL长连接,-看6.3;
4. 设置周期发送同步,-看6.4
5. 手动强制同步,-看6.5
例如像Facebook这种项目,除了移动端app,还有web版,假设在web版数据变化了,如何通知到移动端呢?首先这个数据的检查需要自己动手做,Google原生提供了GCM的机制,可以发送notify到移动端,我们只需要在移动端进行监听对应的广播就可以了:
1.判断消息类型,判断是否需要同步
2.调用ContentResolver.requestSync();
@Override
public void onReceive(Context context, Intent intent) {
// Get a GCM object instance
GoogleCloudMessaging gcm =
GoogleCloudMessaging.getInstance(context);
// Get the type of GCM message
String messageType = gcm.getMessageType(intent);
/*
* Test the message type and examine the message contents.
* Since GCM is a general-purpose messaging system, you
* may receive normal messages that don't require a sync
* adapter run.
* The following code tests for a a boolean flag indicating
* that the message is requesting a transfer from the device.
*/
if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)
&&
intent.getBooleanExtra(KEY_SYNC_REQUEST)) {
/*
* Signal the framework to run your sync adapter. Assume that
* app initialization has already created the account.
*/
ContentResolver.requestSync(ACCOUNT, AUTHORITY, null);
...
}
...
}
ContentResolver.requestSync()这个方法最后会调用到ContentService.syncAsUser(),然后调用到SyncManager.scheduleSync()进行同步
当URI对应的数据变化时如何通知:
1. 在配置syncadapter.xml的时候,配了一个ContentProvider的权限;
2. 当该权限对应的ContentProvider的数据变化时候,在客户端处调用ContentResolver.notifyChange(Android.net.Uri,android.database.ContentObserver, boolean)这个方法来通知我们;
3. ContentResolver.notifyChange->getContentService().notifyChange()->ContentService.notifyChange();
下面观察ContentService.notifyChange()的代码如下:
@Override
public void notifyChange(Uri uri, IContentObserver observer,
boolean observerWantsSelfNotifications, int flags,
int userHandle) {
try {
ArrayList calls = new ArrayList();
synchronized (mRootNode) {
mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
flags, userHandle, calls);
}
final int numCalls = calls.size();
for (int i=0; itry {
oc.mObserver.onChange(oc.mSelfChange, uri, userHandle);
if (DEBUG) Slog.d(TAG, "Notified " + oc.mObserver + " of " + "update at "
+ uri);
} catch (RemoteException ex) {
synchronized (mRootNode) {
Log.w(TAG, "Found dead observer, removing");
IBinder binder = oc.mObserver.asBinder();
final ArrayList list
= oc.mNode.mObservers;
int numList = list.size();
for (int j=0; jif (oe.observer.asBinder() == binder) {
list.remove(j);
j--;
numList--;
}
}
}
}
}
if ((flags& ContentResolver.NOTIFY_SYNC_TO_NETWORK) != 0) {
SyncManager syncManager = getSyncManager();
if (syncManager != null) {
syncManager.scheduleLocalSync(null /* all accounts */, callingUserHandle, uid,
uri.getAuthority());
}
}
synchronized (mCache) {
final String providerPackageName = getProviderPackageName(uri);
invalidateCacheLocked(userHandle, providerPackageName, uri);
}
} finally {
restoreCallingIdentity(identityToken);
}
}
上面一共做了2个事情:
1. 对该ContentProvider的所有感兴趣的Observer进行通知;
2. 对SyncManager注册的对该URI感兴趣的syncadapter进行通知。
ContentResolver.setSyncAutomatically(account, "com.crazyman.accountsyncdemo.provider", true);
开启这个选项之后,会在网络连接的状态下进行自动同步
ContentResolver.addPeriodicSync(account, "com.crazyman.accountsyncdemo.provider", Bundle.EMPTY, 60 * 3);
固定间隔一个时间进行自动同步,如上方法,最后一个参数代表时间,单位是秒
Bundle settingsBundle = new Bundle();
settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
flag:
SYNC_EXTRAS_MANUAL
强制发起手动同步,同步框架会忽略当前的一些设置,比如自动同步开关状态。
SYNC_EXTRAS_EXPEDITED
强制立即发起同步。如果不设置这个选项,系统为了优化功耗可能会等待几秒钟,将一段时间内的几次同步合并发起。
一般需要下面几个权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
手动同步和自动同步需要条件:
Provider的isSyncable = true
SyncAdapter的isAlwaysSyncable = trye
自动同步还需要:
系统总同步开关( getMasterSyncAutomatically = true)
SyncAdapter同步开关( getSyncAutomatically = true)
ContentResolver 的notifyChange()发起自动同步时会带SYNC_EXTRAS_UPLOAD标志,Android设计原意是仅将本地数据更新至服务端。此时SyncAdapter中的supportsUploading若是false,则不能发起自动同步。
当文件adapter.xml设置属性 android:userVisible=”true” ,同步设置选项会对用户可见,用户可以选择手动关闭自动同步功能。
在设置-账户-添加账户,会触发对应应用的Authenticator类的addAccount()方法,addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle);
一般情况下在这个方法的bundle带上跳转到另外的账户设置页面intent进行账户添加,当然也可以在这个方法体里面直接做添加账户的操作,然后返回null。这种情况下处理完添加账户的逻辑,该页面不会退出,所以需要调用对应的成功设置方法。
Account account = new Account("联系人", "com.crazyman.accountsyncdemo.type");
AccountManager accountManager = (AccountManager) mContext.getSystemService(ACCOUNT_SERVICE);
boolean isCreated = accountManager.addAccountExplicitly(account, null, null);
//accountAuthenticatorResponse.onResult防止停留在空白画面
if (isCreated) {
Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, "联系人");
result.putString(AccountManager.KEY_ACCOUNT_TYPE, "com.crazyman.accountsyncdemo.type");
accountAuthenticatorResponse.onResult(result);
}
return null;
参考来自:
http://blog.csdn.net/inconsolabl/article/details/48054341
http://www.jianshu.com/p/dc9a2693478e
http://www.2cto.com/kf/201411/352718.html
http://www.cnblogs.com/fengzhblog/p/3177002.html