SyncAdapterの実装メモ

# 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`において、特定の種類の<intent-filter>を持ったサービスが存在するかどうかでチェックされます。
同期設定を編集するための権限として、`WRITE_SYNC_SETTINGS`が必要な他、アカウント取得とトークン取得のために`GET_ACCOUNTS`、`USE_CREDENTIALS`も必要になります。
また、注意点として`<provider>`タグに同期対象であることを示す`android:syncable`の設定が必要です。

```AndroidManifest.xml
<?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>
```

<service>タグの<intent-filter>と<meta-data>の記述は`Authenticator`アプリの実装に似ています。
`SyncAdapter`ではそれに加えて`android:process=":sync"`という属性の宣言が必要なので、注意してください。
<meta-data>から参照されるXMLファイルには`SyncAdapter`に関する宣言を記述します。

```/res/xml/syncadapter.xml
<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が同時に実行されることを許すかどうか。<br />複数のアカウントと同期したい場合に設定する。|
|android:contentAuthority|同期対象の`ContentProvider`の`authority`と同じものにする|
|android:isAlwaysSyncable|常に同期が実行可能な常態かどうかを示すフラグ。手動同期のみの場合は`false`。|
|android:supportsUploading|同期作業でデータのアップロード行うかどうかのフラグ。<br>`true`の場合、`ContentResolver#notifyChange()`されていると同期が必要と見なすようだ|
|android:userVisible|設定アプリのアカウントと同期にこのアプリの同期設定が表示されるかどうかのフラグ。<br />`true`にするとユーザが同期設定を解除できるので注意。|

## SyncAdapterの実装
`SyncAdapter`は`ContentResolver`によって実行される同期処理そのものを記述するクラスです。
必要なメソッドは抽象クラスである`AbstractThreadedSyncAdapter`で宣言されているため、このクラスを継承して実装します。
基本的には`AbstractThreadedSyncAdapter#onPerformSync()`メソッドのみ実装すれば良く、このメソッドの中でトークンの取得から通信処理、`ContentProvider`の更新処理を記述します。
また、このメソッドからトークンを取得するためには`AccountManager#blockingGetAuthToken()`というメソッドを利用します。
このメソッドはトークンが取得できるまでの間処理をブロックするためメインスレッドでは使えませんが、`AbstractThreadedSyncAdapter#onPerformSync()`はバックグラウンドで処理するメソッドなので大丈夫です。

```MySyncAdapter.java
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()を結果を返すだけです。

```MySyncService.java
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()`を使用し、間隔を秒単位で指定します。

```MyActivity.java
    @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()`を利用して既存設定を削除する必要があります。

```MyActivity.java
    @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`の動作は同期間隔の指定やネットワークの効率的利用によってバッテリーの消費が抑えられていますが、
このメソッドを使用すると同期処理を強制的に行ってしまうため、バッテリーを過剰に消費する場合があります。

```java
    @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()`したほうが良いかもしれません。
当然ですが、その際取得したトークンを保存してはいけません。

<img src="https://qiita-image-store.s3.amazonaws.com/0/48019/3631de01-4d40-2452-ab1a-25c5f7476d80.png" width="360" /><img src="https://qiita-image-store.s3.amazonaws.com/0/48019/7f86b1a0-6781-f3e7-5044-b998a6df9956.png" width="360" />