SyncAdapterの実装メモ

SyncAdapterとは

SyncAdapterとは、AccountManager上のアカウントとContentProviderを紐付けることにより、クラウド上のデータと端末のデータを同期させる仕組みです。 一般的な動作としては「ContentResolverAccountManagerのアカウントとSyncAdapterを登録し、指定した周期でAccountManagerから取得した認証トークンを利用して通信処理を行い、ContentProviderを更新する」という挙動となります。 このとき、同期処理を管理・実行するのはContentResolverであり、AccountManagerはアカウントとトークンの取得にのみ利用します。

よくある誤解

SyncAdapterAccountManager上のaccountTypeContentProviderauthorityを利用して紐付けを行う仕組みですが、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において、特定の種類の<intent-filter>を持ったサービスが存在するかどうかでチェックされます。 同期設定を編集するための権限として、WRITE_SYNC_SETTINGSが必要な他、アカウント取得とトークン取得のためにGET_ACCOUNTSUSE_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>

タグの<intent-filter>と<meta-data>の記述はAuthenticatorアプリの実装に似ています。 SyncAdapterではそれに加えてandroid:process=":sync"という属性の宣言が必要なので、注意してください。 <meta-data>から参照される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 同期対象のContentProviderauthorityと同じものにする
android:isAlwaysSyncable 常に同期が実行可能な常態かどうかを示すフラグ。手動同期のみの場合はfalse
android:supportsUploading 同期作業でデータのアップロード行うかどうかのフラグ。
trueの場合、ContentResolver#notifyChange()されていると同期が必要と見なすようだ
android:userVisible 設定アプリのアカウントと同期にこのアプリの同期設定が表示されるかどうかのフラグ。
trueにするとユーザが同期設定を解除できるので注意。

SyncAdapterの実装

SyncAdapterContentResolverによって実行される同期処理そのものを記述するクラスです。 必要なメソッドは抽象クラスである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();
        }
    }
}

同期サービスの実装

認証サービスはContentResolverSyncAdapterとの橋渡しを行います。 実装的には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を使用する必要があります。

アプリの承認

ActivityAccountManager#getAuthToken()せず、SyncAdapterではじめてAccountManager#blockingGetAuthToken()した場合、アプリの接続承認のための通知が表示されます。 この通知をタップすると承認画面へ遷移し、そこで承認してはじめて同期処理でトークンを利用できるようになりますが、若干わかりづらいのでActivity側でアカウント選択時にAccountManager#getAuthToken()したほうが良いかもしれません。 当然ですが、その際取得したトークンを保存してはいけません。