同期アダプターを作成する

注: バックグラウンド処理ユースケースのほとんどで、WorkManager を推奨ソリューションとしておすすめしています。最適なソリューションについては、バックグラウンド処理ガイドをご覧ください。

アプリ内の同期アダプター コンポーネントは、デバイスとサーバーの間でデータを転送するタスクのコードをカプセル化します。同期アダプター フレームワークでは、アプリで指定したスケジュールとトリガーに基づき、同期アダプター コンポーネント内のコードが実行されます。同期アダプター コンポーネントをアプリに追加するには、次のものを追加する必要があります。

同期アダプター クラス。
同期アダプター フレームワークと互換性のあるインターフェースでデータ転送コードをラップするクラス。
バインドされた Service
同期アダプター フレームワークが同期アダプター クラスのコードを実行できるようにするコンポーネント。
同期アダプターの XML メタデータ ファイル。
同期アダプターに関する情報が含まれるファイル。フレームワークは、このファイルを読み取って、データ転送の読み取り方法とスケジュール設定を確認します。
アプリ マニフェストの宣言。
バインドされたサービスを宣言し、同期アダプター固有のメタデータを指定する XML。

このレッスンでは、これらの要素を定義する方法を説明します。

同期アダプター クラスを作成する

このセクションでは、データ転送コードをカプセル化する同期アダプター クラスの作成方法について説明します。クラスを作成するには、同期アダプターの基本クラスを拡張し、クラスのコンストラクタを定義して、データ転送タスクを定義するメソッドを実装します。

基本同期アダプター クラスを拡張する

同期アダプター コンポーネントを作成するには、まず AbstractThreadedSyncAdapter を拡張し、そのコンストラクタを記述します。同期アダプター コンポーネントが一から作成されるたびに、コンストラクタを使用してセットアップ タスクを実行します(アクティビティのセットアップに Activity.onCreate() を使用するのと同様)。たとえば、アプリのデータがコンテンツ プロバイダを使って格納されている場合は、コンストラクタを使用して ContentResolver インスタンスを取得します。Android プラットフォーム バージョン 3.0 では、parallelSyncs 引数をサポートするために 2 つ目のコンストラクタ形式が追加されているため、互換性を維持するには、2 つのコンストラクタ形式を作成する必要があります。

注: 同期アダプター フレームワークは、シングルトン インスタンスである同期アダプター コンポーネントを操作できるように設計されています。同期アダプター コンポーネントのインスタンス化について詳しくは、同期アダプターをフレームワークにバインドするをご覧ください。

次の例は、AbstractThreadedSyncAdapter とそのコンストラクタを実装する方法を示しています。

Kotlin

    /**
     * Handle the transfer of data between a server and an
     * app, using the Android sync adapter framework.
     */
    class SyncAdapter @JvmOverloads constructor(
            context: Context,
            autoInitialize: Boolean,
            /**
             * Using a default argument along with @JvmOverloads
             * generates constructor for both method signatures to maintain compatibility
             * with Android 3.0 and later platform versions
             */
            allowParallelSyncs: Boolean = false,
            /*
             * If your app uses a content resolver, get an instance of it
             * from the incoming Context
             */
            val mContentResolver: ContentResolver = context.contentResolver
    ) : AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
        ...
    }
    

Java

    /**
     * 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 contentResolver;
        /**
         * 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
             */
            contentResolver = 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
             */
            contentResolver = context.getContentResolver();
            ...
        }
    

データ転送コードを追加する

同期アダプター コンポーネントでは、データ転送は自動的には行われません。代わりに、データ転送コードがカプセル化されるため、同期アダプター フレームワークでは、アプリからの関与なしにデータ転送をバックグラウンドで実行できます。フレームワークではアプリのデータ同期の準備ができたら、onPerformSync() メソッドの実装が呼び出されます。

メインのアプリコードから同期アダプター コンポーネントへのデータ転送を容易にするために、同期アダプター フレームワークでは、次の引数を使用して onPerformSync() が呼び出されます。

アカウント
同期アダプターをトリガーしたイベントに関連付けられた Account オブジェクト。サーバーでアカウントが使用されていない場合は、このオブジェクトの情報を使用する必要はありません。
エクストラ
同期アダプターをトリガーしたイベントによって送信されたフラグが含まれる Bundle
認証局
システムのコンテンツ プロバイダの認証局。アプリはこのプロバイダにアクセスできる必要があります。通常、認証局はご自身のアプリのコンテンツ プロバイダに対応します。
コンテンツ プロバイダ クライアント
認証局引数によって指定されたコンテンツ プロバイダの ContentProviderClientContentProviderClient は、コンテンツ プロバイダへの軽量の公開インターフェースで、基本的な機能は ContentResolver と同じです。コンテンツ プロバイダを使用してアプリのデータを格納する場合は、このオブジェクトでプロバイダに接続できます。それ以外の場合は、無視できます。
同期結果
同期アダプター フレームワークに情報を送信するときに使用する SyncResult オブジェクト。

次のスニペットは、onPerformSync() の全体的な構造を示しています。

Kotlin

    /*
     * 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 fun onPerformSync(
            account: Account,
            extras: Bundle,
            authority: String,
            provider: ContentProviderClient,
            syncResult: SyncResult
    ) {
        /*
         * Put the data transfer code here.
         */
    }
    

Java

    /*
     * 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.
         */
    }
    

onPerformSync() の実際の実装はアプリのデータ同期要件とサーバー接続プロトコルによって異なりますが、実装によって実行しなければならない一般的なタスクもいくつかあります。

サーバーへの接続
データ転送を開始するときにネットワークを使用できると見なすことができても、同期アダプター フレームワークは自動的にはサーバーに接続されません。
データのダウンロードとアップロード
同期アダプターではデータ転送タスクが自動化されません。サーバーからデータをダウンロードしてコンテンツ プロバイダに格納するには、データのリクエスト、ダウンロード、プロバイダへの挿入を行うコードを提供する必要があります。同様に、サーバーにデータを送信する場合は、ファイル、データベース、プロバイダからデータを読み取り、必要なアップロード リクエストを送信する必要があります。また、データの転送中に発生するネットワーク エラーも処理する必要があります。
データ競合の処理またはデータの新しさの判断
同期アダプターでは、サーバー上のデータとデバイス上のデータの競合が自動的に処理されません。また、サーバー上のデータがデバイス上のデータより新しいかどうかも、自動的には検出されません。このため、この状況に対応するアルゴリズムをご自身で提供する必要があります。
クリーンアップ
必ずサーバーへの接続を終了し、データ転送の終了時に一時ファイルとキャッシュをクリーンアップします。

注: 同期アダプター フレームワークではバックグラウンド スレッドで onPerformSync() が実行されるため、ご自身でバックグラウンド処理をセットアップする必要はありません。

同期関連のタスクのほかに、通常のネットワーク関連タスクを組み合わせて、onPerformSync() に追加してみてください。 すべてのネットワーク タスクをこのメソッドに集中させることで、ネットワーク インターフェースの起動と停止に必要な電池の消費量を抑えることができます。ネットワーク アクセスの効率化について詳しくは、電池を消耗しないデータ転送のトレーニング クラスをご覧ください。このトレーニング クラスでは、データ転送コードに含めることができるネットワーク アクセスタスクをいくつか取り上げて説明します。

同期アダプターをフレームワークにバインドする

データ転送コードは同期アダプター コンポーネントにカプセル化されますが、フレームワークがコードにアクセスできるようにする必要はあります。それには、同期アダプター コンポーネントから特別な Android バインダー オブジェクトをフレームワークに渡す、バインドされた Service を作成する必要があります。このバインダー オブジェクトを使って、フレームワークは onPerformSync() メソッドを呼び出してデータを渡すことができます。

同期アダプター コンポーネントは、サービスの onCreate() メソッドでシングルトンとしてインスタンス化します。onCreate() でコンポーネントをインスタンス化すると、サービスが開始するまでコンポーネントの作成が延期されます。サービスが開始するのは、フレームワークがデータ転送を最初に実行しようとしたときです。トリガーまたはスケジュール設定に応じて、同期アダプター フレームワークが同期アダプターの複数の実行をキューに追加する場合に備えて、コンポーネントはスレッドセーフな方法でインスタンス化する必要があります。

たとえば、次のスニペットは、バインドされた Service を実装し、同期アダプター コンポーネントをインスタンス化して、Android バインダー オブジェクトを取得するクラスを作成する方法を示しています。

Kotlin

    package com.example.android.syncadapter
    /**
     * Define a Service that returns an [android.os.IBinder] for the
     * sync adapter class, allowing the sync adapter framework to call
     * onPerformSync().
     */
    class SyncService : Service() {
        /*
         * Instantiate the sync adapter object.
         */
        override fun onCreate() {
            /*
             * Create the sync adapter as a singleton.
             * Set the sync adapter as syncable
             * Disallow parallel syncs
             */
            synchronized(sSyncAdapterLock) {
                sSyncAdapter = sSyncAdapter ?: SyncAdapter(applicationContext, true)
            }
        }

        /**
         * Return an object that allows the system to invoke
         * the sync adapter.
         *
         */
        override fun onBind(intent: Intent): IBinder {
            /*
             * 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()
             *
             * We should never be in a position where this is called before
             * onCreate() so the exception should never be thrown
             */
            return sSyncAdapter?.syncAdapterBinder ?: throw IllegalStateException()
        }

        companion object {
            // Storage for an instance of the sync adapter
            private var sSyncAdapter: SyncAdapter? = null
            // Object to use as a thread-safe lock
            private val sSyncAdapterLock = Any()
        }
    }
    

Java

    package com.example.android.syncadapter;
    /**
     * Define a Service that returns an <code><a href="/reference/android/os/IBinder.html">IBinder</a></code> 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();
        }
    }
    

注: 同期アダプターのバインドされたサービスの詳細な例については、サンプルアプリをご覧ください。

フレームワークで必要なアカウントを追加する

同期アダプター フレームワークでは、各同期アダプターにアカウントの種類が必要です。アカウントの種類の値は、認証システムのメタデータ ファイルを追加するセクションで宣言したものです。ここでは Android システムでこのアカウントの種類をセットアップする必要があります。アカウントの種類をセットアップするには、addAccountExplicitly() を呼び出して、アカウントの種類を使用するダミー アカウントを追加します。

このメソッドは、アプリの開始アクティビティの onCreate() メソッドで呼び出すことをおすすめします。次のコード スニペットは、これを行う方法を示しています。

Kotlin

    ...
    // Constants
    // The authority for the sync adapter's content provider
    const val AUTHORITY = "com.example.android.datasync.provider"
    // An account type, in the form of a domain name
    const val ACCOUNT_TYPE = "example.com"
    // The account name
    const val ACCOUNT = "dummyaccount"
    ...
    class MainActivity : FragmentActivity() {

        // Instance fields
        private lateinit var mAccount: Account
        ...
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
           ...
            // Create the dummy account
            mAccount = createSyncAccount()
           ...
        }
        ...
        /**
         * Create a new dummy account for the sync adapter
         */
        private fun createSyncAccount(): Account {
            val accountManager = getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
            return Account(ACCOUNT, ACCOUNT_TYPE).also { newAccount ->
                /*
                 * 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.
                     */
                }
            }
        }
        ...
    }
    

Java

    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.
                 */
            }
        }
        ...
    }
    

同期アダプターのメタデータ ファイルを追加する

同期アダプター コンポーネントをフレームワークに接続するには、コンポーネントを記述して追加フラグを提供するメタデータをフレームワークに提供する必要があります。メタデータでは、同期アダプター用に作成したアカウントの種類が指定され、アプリに関連付けられたコンテンツ プロバイダ認証局が宣言されます。また、同期アダプターに関連するシステム ユーザー インターフェースの一部が制御されるほか、その他の同期関連フラグが宣言されます。このメタデータは、アプリ プロジェクトの /res/xml/ ディレクトリにある特別な XML ファイルで宣言してください。ファイルには任意の名前を指定できますが、通常は syncadapter.xml と呼ばれます。

この XML ファイルには、以下の属性を持つ単一の XML 要素 <sync-adapter> が含まれます。

android:contentAuthority
コンテンツ プロバイダの URI 認証局。前のスタブ コンテンツ プロバイダの作成のレッスンでアプリに対してスタブ コンテンツ プロバイダを作成した場合は、アプリ マニフェストに追加した <provider> 要素の属性 android:authorities に対して指定した値を使用します。この属性について詳しくは、マニフェストでプロバイダを宣言するをご覧ください。
同期アダプターを使用してコンテンツ プロバイダからサーバーにデータを転送する場合、この値は、そのデータに対して使用しているコンテンツ URI 認証局と同じにする必要があります。この値は、アプリ マニフェストでプロバイダを宣言する <provider> 要素の android:authorities 属性で指定する認証局の 1 つである必要もあります。
android:accountType
同期アダプター フレームワークで必要なアカウントの種類。認証システムのメタデータ ファイルを追加するセクションで説明したように、この値は、認証システムのメタデータ ファイルで作成したときに指定したアカウントの種類の値と同じである必要があります。また、フレームワークで必要なアカウントを追加するセクションのコード スニペットの定数 ACCOUNT_TYPE に指定した値でもあります。
設定属性
android:userVisible
同期アダプターのアカウントの種類の表示を設定します。アカウントの種類に関連付けられているアカウント アイコンとラベルは、デフォルトでは、システムの設定アプリのアカウント セクションに表示されます。このため、アプリに簡単に関連付けられるアカウントの種類またはドメインがない限り、同期アダプターは非表示にする必要があります。アカウントの種類を非表示にしても、ユーザーがアプリのアクティビティのいずれかのユーザー インターフェースを使用して、同期アダプターを制御できるようにすることは可能です。
android:supportsUploading
データをクラウドにアップロードできるようにします。データのダウンロードのみをアプリで行う場合は、これを false に設定します。
android:allowParallelSyncs
同期アダプター コンポーネントの複数のインスタンスを同時に実行できるようにします。 アプリで複数のユーザー アカウントがサポートされ、複数のユーザーが同時にデータを転送できるようにする場合に使用します。複数のデータ転送を実行しない場合、このフラグは動作に影響しません。
android:isAlwaysSyncable
指定したタイミングで同期アダプターを実行できることを、同期アダプター フレームワークに示します。同期アダプターを実行できるタイミングをプログラムで制御するには、このフラグを false に設定し、requestSync() を呼び出して同期アダプターを実行します。同期アダプターの実行について詳しくは、同期アダプターの実行をご覧ください。

次の例は、1 つのダミー アカウントを使用してダウンロードのみを行う同期アダプターの XML を示しています。

    <?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"/>
    

マニフェストで同期アダプターを宣言する

同期アダプター コンポーネントをアプリに追加したら、コンポーネントの使用に関連する権限をリクエストし、追加したバインドされた Service を宣言する必要があります。

同期アダプター コンポーネントでは、ネットワークとデバイスの間でデータを転送するコードが実行されるため、インターネットへのアクセス権限をリクエストする必要があります。また、他のコンポーネントから同期アダプターをプログラムによって制御できるように、アプリでは同期アダプター設定の読み取りおよび書き込み権限をリクエストする必要があります。スタブ認証システムの作成のレッスンで作成した認証システム コンポーネントを、アプリが使用できるようにする特別な権限をリクエストする必要もあります。

これらの権限をリクエストするには、アプリ マニフェストに <manifest> の子要素として次を追加します。

android.permission.INTERNET
データのダウンロードまたはデバイスからサーバーへのデータのアップロードができるように、同期アダプター コードによるインターネット アクセスを許可します。この権限を前にリクエストしている場合、追加する必要はありません。
android.permission.READ_SYNC_SETTINGS
アプリによる現在の同期アダプター設定の読み取りを許可します。たとえば、getIsSyncable() を呼び出すにはこの権限が必要です。
android.permission.WRITE_SYNC_SETTINGS
アプリによる同期アダプター設定の制御を許可します。addPeriodicSync() を使用して同期アダプターの定期的な実行を設定するには、この権限が必要です。この権限は requestSync() の呼び出しには不要です。同期アダプターの実行について詳しくは、同期アダプターの実行をご覧ください。

次のスニペットは、権限を追加する方法を示しています。

    <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>
    

最後に、フレームワークが同期アダプターとやり取りするときに使用するバインドされた Service を宣言するには、次の XML を <application> の子要素としてアプリ マニフェストに追加します。

            <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>
    

<intent-filter> 要素により、同期アダプターを実行するために送信されたインテント アクション android.content.SyncAdapter によってトリガーされたフィルタがセットアップされます。フィルタがトリガーされると、作成したバインドされたサービス(この例では SyncService)が開始されます。属性 android:exported="true" を使用すると、アプリ以外のプロセス(システムを含む)が Service にアクセスできるようになります。属性 android:process=":sync" は、sync という名前のグローバル共有プロセスで Service を実行するようシステムに指示します。アプリ内に複数の同期アダプターがある場合、そのアダプターはこのプロセスを共有できるため、オーバーヘッドが減ります。

<meta-data> 要素によって、前に作成した同期アダプターのメタデータ XML ファイルの名前が提供されます。 android:name 属性は、このメタデータが同期アダプター フレームワークを対象としていることを示します。android:resource 要素により、メタデータ ファイルの名前が指定されます。

これで、同期アダプターのすべてのコンポーネントが揃いました。次のレッスンでは、イベントへの応答として、または定期的に同期アダプターを実行するよう同期アダプター フレームワークに指示する方法について説明します。