SyncAdapterとは
SyncAdapter
とは、AccountManager
上のアカウントとContentProvider
を紐付けることにより、クラウド上のデータと端末のデータを同期させる仕組みです。
一般的な動作としては「ContentResolver
にAccountManager
のアカウントとSyncAdapter
を登録し、指定した周期でAccountManager
から取得した認証トークンを利用して通信処理を行い、ContentProvider
を更新する」という挙動となります。
このとき、同期処理を管理・実行するのはContentResolver
であり、AccountManager
はアカウントとトークンの取得にのみ利用します。
よくある誤解
SyncAdapter
はAccountManager
上のaccountType
とContentProvider
のauthority
を利用して紐付けを行う仕組みですが、SyncAdapter
を実装したアプリがアカウントの管理機能(Authenticator
)を持つ必要はありません。
例えば、SyncAdapter
が対象とするaccountType
を"com.google"とすることでGoogleアカウントに自アプリのSyncAdapter
を登録することもできます。
この記事中のサンプルでも実際にGoogleアカウントにSyncAdapter
を紐付け、同期処理を実行させています。
SyncAdapterの実装
SyncAdapter
の実装はAuthenticator
の実装と少し似ています。
Authenticator
同様、システムから実行されるService
と、その実態(IBinder
)であるSyncAdapter
、そしてSyncAdapter
の宣言に用いるXMLファイルなどが必要なほか、同期するデータの置き場所としてContentProvider
の宣言も必要です。
なお、ContentProvider
の実装部分は通常のアプリと全く変わりがないため、今回のサンプルでは、ContentProvider
の実装については詳しく説明しません。
SyncAdapterの宣言
SyncAdapter
による同期機能を提供するためには、SyncAdapter
を実装したアプリであることをシステムに宣言する必要があります。
この宣言はAndroidManifest.xml
において、特定の種類のを持ったサービスが存在するかどうかでチェックされます。
同期設定を編集するための権限として、WRITE_SYNC_SETTINGS
が必要な他、アカウント取得とトークン取得のためにGET_ACCOUNTS
、USE_CREDENTIALS
も必要になります。
また、注意点として<provider>
タグに同期対象であることを示すandroid:syncable
の設定が必要です。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.nein37.syncadaptersample" >
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<activity
android:name=".MyActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:exported="false"
android:syncable="true"
android:authorities="com.example.nein37.syncadaptersample"
android:name=".MyContentProvider" />
<service
android:name=".MySyncService"
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>
</application>
</manifest>
タグのとの記述はAuthenticator
アプリの実装に似ています。
SyncAdapter
ではそれに加えてandroid:process=":sync"
という属性の宣言が必要なので、注意してください。
から参照されるXMLファイルにはSyncAdapter
に関する宣言を記述します。
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.google"
android:allowParallelSyncs="false"
android:contentAuthority="com.example.nein37.syncadaptersample"
android:isAlwaysSyncable="true"
android:supportsUploading="false"
android:userVisible="true" />
ここで設定している属性は、それぞれ以下の様な意味です。
属性名 | 意味 |
---|---|
android:accountType | 対象とするアカウントのaccountType
|
android:allowParallelSyncs | 複数のSyncAdapterが同時に実行されることを許すかどうか。 複数のアカウントと同期したい場合に設定する。 |
android:contentAuthority | 同期対象のContentProvider のauthority と同じものにする |
android:isAlwaysSyncable | 常に同期が実行可能な常態かどうかを示すフラグ。手動同期のみの場合はfalse 。 |
android:supportsUploading | 同期作業でデータのアップロード行うかどうかのフラグ。true の場合、ContentResolver#notifyChange() されていると同期が必要と見なすようだ |
android:userVisible | 設定アプリのアカウントと同期にこのアプリの同期設定が表示されるかどうかのフラグ。true にするとユーザが同期設定を解除できるので注意。 |
SyncAdapterの実装
SyncAdapter
はContentResolver
によって実行される同期処理そのものを記述するクラスです。
必要なメソッドは抽象クラスであるAbstractThreadedSyncAdapter
で宣言されているため、このクラスを継承して実装します。
基本的にはAbstractThreadedSyncAdapter#onPerformSync()
メソッドのみ実装すれば良く、このメソッドの中でトークンの取得から通信処理、ContentProvider
の更新処理を記述します。
また、このメソッドからトークンを取得するためにはAccountManager#blockingGetAuthToken()
というメソッドを利用します。
このメソッドはトークンが取得できるまでの間処理をブロックするためメインスレッドでは使えませんが、AbstractThreadedSyncAdapter#onPerformSync()
はバックグラウンドで処理するメソッドなので大丈夫です。
public class MySyncAdapter extends AbstractThreadedSyncAdapter {
ContentResolver mContentResolver;
public MySyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContentResolver = context.getContentResolver();
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
try {
AccountManager manager =AccountManager.get(getContext());
// トークンの取得
String token = manager.blockingGetAuthToken(account, "cl", true);
// TODO ここで通信処理を行う
// TODO DBへの反映処理を行う
ContentValues values = new ContentValues();
values.put(COLUMN, VALUE);
getContext().getContentResolver().insert(CONTENT_URI, values);
} catch (OperationCanceledException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AuthenticatorException e) {
e.printStackTrace();
}
}
}
同期サービスの実装
認証サービスはContentResolver
とSyncAdapter
との橋渡しを行います。
実装的にはService#onBind()
でAbstractThreadedSyncAdapter#getSyncAdapterBinder()を結果を返すだけです。
public class MySyncService extends Service {
private MySyncAdapter mSyncAdapter;
@Override
public void onCreate() {
super.onCreate();
mSyncAdapter =new MySyncAdapter(this,true);
}
@Override
public IBinder onBind(Intent intent) {
return mSyncAdapter.getSyncAdapterBinder();
}
}
SyncAdapterの登録
SyncAdapter
による同期を行うためには、ContentResolver
へのSyncAdapter
の登録が必要です。
通常、SyncAdapter
の登録はアプリ内でのログイン/アカウント選択時や設定画面などで行われますが、サンプルでは単純にアプリ起動時に一番最初に登録されたGoogleアカウントに対して登録を行います。
SyncAdapter
の登録メソッドは同期処理のタイミングによっていくつか種類があるので、以下それぞれ説明します。
定期的に同期を行いたい場合
何時間おき、何日おきというように間隔を指定して定期的に同期処理を実行したい場合、ContentResolver.addPeriodicSync()
を使用し、間隔を秒単位で指定します。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
// 今回は最初に登録されたGoogleアカウントが対象
Account account = AccountManager.get(this).getAccountsByType("com.google")[0];
Bundle args = new Bundle();
// 必要なパラメータがあればBundleに詰める
// 10分ごとに同期
ContentResolver.addPeriodicSync(account, AUTHORITY, args, 600);
}
ネットワークメッセージのタイミングで同期を行いたい場合
Androidシステムはネットワークに接続の確立後、TCP/IPコネクションを維持するために短い間隔でメッセージ送出しています。
このメッセージが送出されたタイミングで同期を行う場合、ContentResolver.setSyncAutomatically()
を使用します。
実際にはこの設定に関する挙動はもう少し複雑で、android:supportsUploading
設定やContentResolver#notifyChange()
などで更新が必要かどうかを判断しているようです。
そのため、ContentProvider
まわりの実装にミスがあるとうまく同期されなくなるかもしれません。
また、ContentResolver.setSyncAutomatically()
を使用した場合でもContentResolver.addPeriodicSync()
で登録した内容は削除されないことに注意してください。
ContentResolver.addPeriodicSync()
した後でネットワークメッセージタイミングのみの同期に切り替えたい場合、ContentResolver.removePeriodicSync()
を利用して既存設定を削除する必要があります。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
// 今回は最初に登録されたGoogleアカウントが対象
Account account = AccountManager.get(this).getAccountsByType("com.google")[0];
// ネットワークメッセージタイミングで同期
ContentResolver.setSyncAutomatically(account, AUTHORITY, true);
}
任意のタイミングで同期を行いたい場合
ボタン押下時など、任意のタイミングで同期を行う場合、ContentResolver.requestSync()
を使用できます。
通常、SyncAdapter
の動作は同期間隔の指定やネットワークの効率的利用によってバッテリーの消費が抑えられていますが、
このメソッドを使用すると同期処理を強制的に行ってしまうため、バッテリーを過剰に消費する場合があります。
@Override
public void onClick(View v) {
Account account = AccountManager.get(this).getAccountsByType("com.google")[0];
ContentResolver.requestSync(account,SampleColumn.AUTHORITY,new Bundle());
}
注意点
SyncAdapter
は非常に優れた機能ですが、実装する際に注意すべきことが多い上に資料が少ないのが難点です。
ここでは、私が特に注意すべきと感じたことをまとめます。
AndroidManifest.xmlの設定
SyncAdapter
を使う場合、<service>
だけでなく<provider>
にも設定が必要なことに注意が必要です。
また、android:process=":sync"
をandroid:process="sync"
と書いてしまったり、<service>
をandroid:exported=false
にしてしまったりすると動作しません。
android:userVisible="false"
SyncAdapter
の設定をandroid:userVisible="false"
にすると、設定アプリの同期一覧で表示されなくなります。
この設定はユーザにSyncAdapter
を無効にさせないために有用ですが、SyncAdapter
の設定を判別することができなくなります。
特に定期同期とネットワークメッセージ同期は片方を設定してももう片方が解除されないため、意図せず頻繁に同期されてしまう可能性があります。
SyncAdapter
を設定する際はContentResolver.getPeriodicSyncs()
やContentResolver.getSyncAutomatically()
でSyncAdapter
の設定状況を取得し、不要な設定情報が存在しないか確認したほうが良いかもしれません。
APIレベル
SyncAdapter
の大半の機能はAPI level 5から実装されていますが、ContentResolver.addPeriodicSync()
などの定期同期のための機能はAPI level 8以降となります。
そのため、API level 5-7 の端末で定期同期を行いたい場合、AlarmManager
を使用する必要があります。
アプリの承認
Activity
でAccountManager#getAuthToken()
せず、SyncAdapter
ではじめてAccountManager#blockingGetAuthToken()
した場合、アプリの接続承認のための通知が表示されます。
この通知をタップすると承認画面へ遷移し、そこで承認してはじめて同期処理でトークンを利用できるようになりますが、若干わかりづらいのでActivity
側でアカウント選択時にAccountManager#getAuthToken()
したほうが良いかもしれません。
当然ですが、その際取得したトークンを保存してはいけません。