2017-10-15 | learn

android-sync-adapter

SyncAdapter


相对于自己实现配置同步的优点:

  • 插件架构,以 callable 组件形式向 system 添加数据传输代码。
  • 自动执行,可以配置不同的规则自动同步配置,包括数据变化,时间周期或每天指定时间更新。当传输不可用时系统会自动安排重试。
  • 自动网络检查,当网络可用时才会执行。
  • 电池性能优化
  • 账户管理和权限

Note: Sync adapters 异步执行,适用于周期高效的数据传输,不适用于即时通信这些队实时性要求高的场景。

Authenticator

Sync adapter 框架假定你使用一个帐号登录服务器保存需要在不同设备间传递的数据。所以 sync adapter 需要一个 authenticator 组件,植入 Android 帐号和验证框架,提供标准接口进行用户登录等操作。

即使你的 app 没有用 account,也要提供这个组件。当不需要 account 和 server login 时, 相关认证信息会被忽略,所以你可以只提供一个实现了所有方法的空 authenticator 组件。你还要提供个 bound Service,给 sync adapter 框架调用 authenticator 的方法。

Stub Authenticator Component

空 Authenticator 只需要实现方法,返回 null,抛出异常即可。

代码这里有 -> https://developer.android.com/training/sync-adapters/creating-authenticator.html#CreateAuthenticatorService

Bind the Authenticator to the Framework

Sync adapter framework 需要一个 Service 访问 authenticator。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* A bound Service that instantiates the authenticator
* when started.
*/
public class AuthenticatorService extends Service {
...
// Instance field that stores the authenticator object
private Authenticator mAuthenticator;
@Override
public void onCreate() {
// Create a new authenticator object
mAuthenticator = new Authenticator(this);
}
/*
* When the system binds to this Service to make the RPC call
* return the authenticator's IBinder.
*/
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}

Add the Authenticator Metadata File

我们还要给 sync adapter 和 account 提供些额外信息,使用 metadata。此文件应位于 /res/xml/ 目录下,通常命名为 authenticator.xml

具体配置项:

android:accountType

​ 域名形式,用作 sync adapter 内部 identification 的组成部分。对于需要登录的 Server,此字段会被同时发送;不需要登录的服务器也要提供,应该使用你名下的域名,此时不会发送此字段到服务器。

android:icon

android:smallIcon

android:label

Example:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="example.com"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/app_name"/>

Declare the Authenticator in the Manifest

1
2
3
4
5
6
7
8
9
<service
android:name="com.example.android.syncadapter.AuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

Creating a Stub Content Provider

Sync adapter 内部设计由灵活安全的 ContentProvider 提供本地数据管理。所以得写个 ContentProvider。没写的话程序就崩了。

Declare the Provider in the Manifest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.network.sync.BasicSyncAdapter"
android:versionCode="1"
android:versionName="1.0" >
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<provider
android:name="com.example.android.datasync.provider.StubProvider"
android:authorities="com.example.android.datasync.provider"
android:exported="false"
android:syncable="true"/>
...
</application>
</manifest>

Creating a Sync Adapter

Sync adapter 组件内就是你在设备和服务器之间传递数据的 task。Sync adapter framework 会根据你在 app 里提供的 scheduling and triggers 执行你的 sync adapter 组件。

组成部分:

  • Sync adapter class.
  • Bound Service.
  • Sync adapter XML metadata file.
  • Declarations in the app manifest.

Extend the base sync adapter class AbstractThreadedSyncAdapter

Note: sync adapter framework 需要 sync adapter 组件是单例形式的。Bind the Sync Adapter to the Framework 有更详细内容。

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
/**
* Handle the transfer of data between a server and an
* app, using the Android sync adapter framework.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter {
...
// Global variables
// Define a variable to contain a content resolver instance
ContentResolver mContentResolver;
/**
* Set up the sync adapter
*/
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
mContentResolver = context.getContentResolver();
}
...
/**
* Set up the sync adapter. This form of the
* constructor maintains compatibility with Android 3.0
* and later platform versions
*/
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
/*
* If your app uses a content resolver, get an instance of it
* from the incoming Context
*/
mContentResolver = context.getContentResolver();
...
}

Add the data transfer code to onPerformSync()

sync adapter component 只有数据传输部分 task,并不负责启动,sync adapter 框架可以在不启动应用的情况下,后台执行数据传输。当准备得当时,framework 会执行 onPerformSync() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
@Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
/*
* Put the data transfer code here.
*/
...
}

几个参数:

  • Account,前面配置的帐号
  • Bundle,触发同步操作时附带的 Bundle 参数。
  • ContentProviderClient, 和基本的 ContentResolver 功能一致。用于与 COntentProvider 通讯。
  • SyncResult,用于向 sync adapter 反馈同步结果。

Note: 无需另开线程,默认在后台线程执行。

Bind the Sync Adapter to the Framework

上面完成了同步部分,我们还要让 sync adapter framework 接管我们的 SyncAadapter。需要提供一个可绑定的 Service 再通过一个特殊的 binder 对象将 sync adapter 组件传递给 framework。framework 根据这个 binder 对象调用执行 onPerformSync() 方法传递数据。

在这个 Service 的 onCreate() 方法中创建 sync adapter 组件的单例对象,这样可以将组件的实例化时机延迟到 framework 第一次尝试执行你的 task 时。实例化要考虑线程安全,以防同一 task 多个 sync 操作同时触发时产生问题。

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
package com.example.android.syncadapter;
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
public class SyncService extends Service {
// Storage for an instance of the sync adapter
private static SyncAdapter sSyncAdapter = null;
// Object to use as a thread-safe lock
private static final Object sSyncAdapterLock = new Object();
/*
* Instantiate the sync adapter object.
*/
@Override
public void onCreate() {
/*
* Create the sync adapter as a singleton.
* Set the sync adapter as syncable
* Disallow parallel syncs
*/
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
/**
* Return an object that allows the system to invoke
* the sync adapter.
*
*/
@Override
public IBinder onBind(Intent intent) {
/*
* Get the object that allows external processes
* to call onPerformSync(). The object is created
* in the base class code when the SyncAdapter
* constructors call super()
*/
return sSyncAdapter.getSyncAdapterBinder();
}
}

Sample App:https://github.com/googlesamples/android-BasicSyncAdapter

Add the Account Required by the Framework

Framework 要求每个 sync adapter 都要有一个 account type。 在上面 Add the Authenticator Metadata File 已经介绍了如何声明。现在需要将其设置到 Android System,调用 addAccountExplicitly()方法。

最好的位置是启动 Activity 的 onCreate() 方法处:

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
public class MainActivity extends FragmentActivity {
...
...
// Constants
// The authority for the sync adapter's content provider
public static final String AUTHORITY = "com.example.android.datasync.provider";
// An account type, in the form of a domain name
public static final String ACCOUNT_TYPE = "example.com";
// The account name
public static final String ACCOUNT = "dummyaccount";
// Instance fields
Account mAccount;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Create the dummy account
mAccount = CreateSyncAccount(this);
...
}
...
/**
* Create a new dummy account for the sync adapter
*
* @param context The application context
*/
public static Account CreateSyncAccount(Context context) {
// Create the account type and default account
Account newAccount = new Account(
ACCOUNT, ACCOUNT_TYPE);
// Get an instance of the Android account manager
AccountManager accountManager =
(AccountManager) context.getSystemService(
ACCOUNT_SERVICE);
/*
* Add the account and account type, no password or user data
* If successful, return the Account object, otherwise report an error.
*/
if (accountManager.addAccountExplicitly(newAccount, null, null)) {
/*
* If you don't set android:syncable="true" in
* in your <provider> element in the manifest,
* then call context.setIsSyncable(account, AUTHORITY, 1)
* here.
*/
} else {
/*
* The account exists or some other error occurred. Log this, report it,
* or handle it internally.
*/
}
}
...
}

Add the Sync Adapter Metadata File

提供 sync adapter metadata 文件。位于 /res/xml/ 文件夹,通常命名为 syncadapter.xml

几个属性:

  • android:contentAuthority :
  • android:accountType :
  • android:userVisible :默认 account icon 和 label 会在设置的 Accounts 项中显示,此项用来控制。即使设置为不可见,你仍可以在 app 中实现自己的 sync adapter 控制界面。
  • android:supportsUploading :允许上传数据,仅下载设置为 false
  • android:allowParallelSyncs :用于多用户,可同时为多个用户同步
  • android:isAlwaysSyncable :标注可在任意时刻执行,如果仅想手动触发,设置为 false,需要同步时手动调用 requestSync()) 函数。

example:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.example.android.datasync.provider"
android:accountType="com.android.example.datasync"
android:userVisible="false"
android:supportsUploading="false"
android:allowParallelSyncs="false"
android:isAlwaysSyncable="true"/>

Declare the Sync Adapter in the Manifest

相关权限:

  • android.permission.INTERNET
  • android.permission.READ_SYNC_SETTINGS
  • android.permission.WRITE_SYNC_SETTINGS

example:

1
2
3
4
5
6
7
8
9
10
11
12
<manifest>
...
<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"/>
...
</manifest>

再用下面这段 xml 将 service 绑定到 framework :

1
2
3
4
5
6
7
8
9
10
<service
android:name="com.example.android.datasync.SyncService"
android:exported="true"
android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>

Running a Sync Adapter

上面已经介绍了创建过程,接下来的内容就是如何执行 sync。最开始我们说到可以定时,周期和手动触发执行 sync,我们建议避免通过用户操作直接触发同步,这样就享受不了 sync adapter framework 的好处了。

你可以使用以下形式的 sync:

  • 服务器端数据改变: 当服务器下发特定 message 时运行 sync adapter 响应,同步服务端的配置。不会影响性能,也不会浪费电池。
  • 本地数据改变: 可将本地新 config 同步至服务器。使用 ContentProvider 实现起来会容易得多。
  • 定期运行:每隔固定时间运行,或者每天指定时间运行
  • 场景需求:响应用户操作,执行同步

Run the Sync Adapter When Server Data Changes

假如你的场景中,服务端的配置经常改变,你可以在数据改变时向终端设备发送特殊消息,客户端接受到消息后调用 ContentResolver.requestSync()) 触发 framework 执行 sync task。

Google Cloud Messaging(GCM) 有提供组件方便 Server 端和 Client 端的开发(用不了用不了)。Gcm 收到消息后会发送一个广播,并且只在数据变动时才会触发请求,而周期执行可能会遇到发出请求数据却并没有变动的尴尬。

The following code snippet shows you how to run requestSync() in response to an incoming GCM message:

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
public class GcmBroadcastReceiver extends BroadcastReceiver {
...
// Constants
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider"
// Account type
public static final String ACCOUNT_TYPE = "com.example.android.datasync";
// Account
public static final String ACCOUNT = "default_account";
// Incoming Intent key for extended data
public static final String KEY_SYNC_REQUEST =
"com.example.android.datasync.KEY_SYNC_REQUEST";
...
@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);
...
}
...
}
...
}

Run the Sync Adapter When Content Provider Data Changes

在 ContentProvider 上注册 observer,数据变动时调用 requestSync()) 。

主要俩函数

  • ContentResolver::registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer) 注册用
  • ContentResolver::notifyChange(Uri uri, ContentObserver observer) 发送用
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
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider scheme
public static final String SCHEME = "content://";
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider";
// Path for the content provider table
public static final String TABLE_PATH = "data_table";
// Account
public static final String ACCOUNT = "default_account";
// Global variables
// A content URI for the content provider's data table
Uri mUri;
// A content resolver for accessing the provider
ContentResolver mResolver;
...
public class TableObserver extends ContentObserver {
/*
* Define a method that's called when data in the
* observed content provider changes.
* This method signature is provided for compatibility with
* older platforms.
*/
@Override
public void onChange(boolean selfChange) {
/*
* Invoke the method signature available as of
* Android platform version 4.1, with a null URI.
*/
onChange(selfChange, null);
}
/*
* Define a method that's called when data in the
* observed content provider changes.
*/
@Override
public void onChange(boolean selfChange, Uri changeUri) {
/*
* Ask the framework to run your sync adapter.
* To maintain backward compatibility, assume that
* changeUri is null.
*/
ContentResolver.requestSync(ACCOUNT, AUTHORITY, null);
}
...
}
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Get the content resolver object for your app
mResolver = getContentResolver();
// Construct a URI that points to the content provider data table
mUri = new Uri.Builder()
.scheme(SCHEME)
.authority(AUTHORITY)
.path(TABLE_PATH)
.build();
/*
* Create a content observer object.
* Its code does not mutate the provider, so set
* selfChange to "false"
*/
TableObserver observer = new TableObserver(false);
/*
* Register the observer for the data table. The table's path
* and any of its subpaths trigger the observer.
*/
mResolver.registerContentObserver(mUri, true, observer);
...
}
...
}

Run the Sync Adapter Periodically

周期执行。

注意几点

  • 一般会把同步放到晚上,插着充电器时
  • 别太实成把所有设备都设置成同一时间,小心服务器爆炸
  • addPeriodicSync()) 并不指定时间,只设置周期
  • 这样你就不能方便的指定每天固定时间更新了不是
  • 你得用 AlarmManager 这东西,在指定时间调用这个方法,周期设成 24h
  • 使用 AlarmManager#setInexactRepeating()) 这个方法记得加随机数,参见第二条
  • addPeriodicSync()) 不会关闭 setSyncAutomatically()) ,所有有可能段时间内多次执行 sync。
  • addPeriodicSync()) 可选的 sync adapter control flags 很少,详情参见文档
  • 感觉很适合天气预报 这种应用啊
  • 再凑一条
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
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider";
// Account
public static final String ACCOUNT = "default_account";
// Sync interval constants
public static final long SECONDS_PER_MINUTE = 60L;
public static final long SYNC_INTERVAL_IN_MINUTES = 60L;
public static final long SYNC_INTERVAL =
SYNC_INTERVAL_IN_MINUTES *
SECONDS_PER_MINUTE;
// Global variables
// A content resolver for accessing the provider
ContentResolver mResolver;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Get the content resolver for your app
mResolver = getContentResolver();
/*
* Turn on periodic syncing
*/
ContentResolver.addPeriodicSync(
ACCOUNT,
AUTHORITY,
Bundle.EMPTY,
SYNC_INTERVAL);
...
}
...
}

Run the Sync Adapter On Demand

原则上这种操作是不可取的,你应该把程序设计为不需要用户进行额外操作(响应用户的号召,让你同步就同步)。

手动调用 ContentResolver.requestSync()

flag:

  • SYNC_EXTRAS_MANUAL :强制执行,framework 忽略当前设置, such as the flag set by setSyncAutomatically().
  • SYNC_EXTRAS_EXPEDITED :强制立即执行,不然系统默认会延迟几秒后执行,出于电池方面的一些考虑。
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
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider authority
public static final String AUTHORITY =
"com.example.android.datasync.provider"
// Account type
public static final String ACCOUNT_TYPE = "com.example.android.datasync";
// Account
public static final String ACCOUNT = "default_account";
// Instance fields
Account mAccount;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
/*
* Create the dummy account. The code for CreateSyncAccount
* is listed in the lesson Creating a Sync Adapter
*/

mAccount = CreateSyncAccount(this);
...
}
/**
* Respond to a button click by calling requestSync(). This is an
* asynchronous operation.
*
* This method is attached to the refresh button in the layout
* XML file
*
* @param v The View associated with the method call,
* in this case a Button
*/
public void onRefreshButtonClick(View v) {
...
// Pass the settings flags by inserting them in a bundle
Bundle settingsBundle = new Bundle();
settingsBundle.putBoolean(
ContentResolver.SYNC_EXTRAS_MANUAL, true);
settingsBundle.putBoolean(
ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
/*
* Request the sync for the default account, authority, and
* manual sync settings
*/
ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
}

ref