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

注: ほとんどのバックグラウンド処理のユースケースで、推奨ソリューションとして 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
権限
システムにおけるコンテンツ プロバイダの権限。アプリはこのプロバイダにアクセスできる必要があります。通常、オーソリティは自身のアプリ内のコンテンツ プロバイダに対応します。
コンテンツ プロバイダ クライアント
Authority 引数で指定されたコンテンツ プロバイダの 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 = "placeholderaccount"
...
class MainActivity : FragmentActivity() {

    // Instance fields
    private lateinit var mAccount: Account
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
       ...
        // Create the placeholder account
        mAccount = createSyncAccount()
       ...
    }
    ...
    /**
     * Create a new placeholder 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 = "placeholderaccount";
    // Instance fields
    Account mAccount;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Create the placeholder account
        mAccount = CreateSyncAccount(this);
        ...
    }
    ...
    /**
     * Create a new placeholder 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="false"
                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="false" を使用すると、アプリとシステムのみが Service にアクセスできるようになります。属性 android:process=":sync" は、sync という名前のグローバル共有プロセスで Service を実行するようシステムに指示します。アプリ内に複数の同期アダプターがある場合、アダプターはこのプロセスを共有できるため、オーバーヘッドを削減できます。

<meta-data> 要素は、前に作成した同期アダプターのメタデータ XML ファイルの名前を指定します。android:name 属性は、このメタデータが同期アダプター フレームワーク用であることを示します。android:resource 要素にはメタデータ ファイルの名前を指定します。

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