同期アダプターを実行する

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

このクラスのこれまでのレッスンでは、データ転送コードをカプセル化する同期アダプター コンポーネントを作成する方法と、同期アダプターをシステムに接続できるコンポーネントを追加する方法を学習しました。これで、同期アダプターを含むアプリをインストールするために必要な準備が整いましたが、これまで見てきたコードはいずれも実際に同期アダプターを実行しません。

スケジュールに基づいて、またはイベントの間接的な結果として、同期アダプターを実行してみてください。たとえば、同期アダプターを一定の時間後または特定の時刻に定期的に実行するよう設定できます。また、デバイスに保存されているデータが変更された場合に、同期アダプターを実行することもできます。ユーザーの操作の直接の結果として同期アダプターを実行しないようにする必要があります。実行すると、同期アダプター フレームワークのスケジューリング機能のメリットを十分に得られないためです。たとえば、ユーザー インターフェースに更新ボタンを配置しないようにします。

同期アダプターを実行するには、次の方法があります。

サーバーデータの変更時
サーバーベースのデータが変更されたことを示すサーバーからのメッセージに応じて、同期アダプターを実行します。このオプションを使用すると、サーバーをポーリングしてパフォーマンスを低下させたり、バッテリーを無駄に消費したりすることなく、サーバーからデバイスにデータを更新できます。
デバイスデータが変更されたときに実行
デバイスでデータが変更されたときに同期アダプターを実行します。このオプションを使用すると、変更されたデータをデバイスからサーバーに送信できるため、サーバーに常に最新のデバイスデータを保持する必要がある場合に特に便利です。コンテンツ プロバイダに実際にデータを保存すれば、このオプションを簡単に実装できます。スタブ コンテンツ プロバイダを使用している場合は、データ変更の検出が困難になることがあります。
一定間隔
指定した間隔が経過した後に同期アダプターを実行するか、毎日特定の時刻に実行します。
オンデマンド
ユーザーの操作に応じて同期アダプターを実行します。ただし、最適なユーザー エクスペリエンスを提供するには、主にいずれかの自動化オプションを使用する必要があります。自動オプションを使用することで、バッテリーとネットワーク リソースを節約できます。

このレッスンの残りの部分では、各オプションについて詳しく説明します。

サーバーデータの変更時に同期アダプターを実行する

アプリがサーバーからデータを転送し、サーバーデータが頻繁に変更される場合は、同期アダプターを使用すると、データの変更に応じてダウンロードを行うことができます。同期アダプターを実行するには、サーバーからアプリ内の BroadcastReceiver に特別なメッセージを送信します。このメッセージへの応答として、ContentResolver.requestSync() を呼び出して、同期アダプター フレームワークに同期アダプターを実行するよう通知します。

Google Cloud Messaging(GCM)には、このメッセージ システムを機能させるために必要なサーバー コンポーネントとデバイス コンポーネントの両方が用意されています。GCM を使用して転送をトリガーするほうが、サーバーのステータスをポーリングするよりも信頼性と効率に優れています。ポーリングには常にアクティブな Service が必要ですが、GCM はメッセージの到着時にアクティブになる BroadcastReceiver を使用します。定期的なポーリングは、利用可能なアップデートがなくてもバッテリーを消費しますが、GCM は必要なときだけメッセージを送信します。

注: GCM を使用して、アプリがインストールされているすべてのデバイスへのブロードキャストを介して同期アダプターをトリガーする場合、メッセージはほぼ同時に送信されます。このような状況で、同期アダプターの複数のインスタンスが同時に実行され、サーバーとネットワークの過負荷が発生する可能性があります。すべてのデバイスへのブロードキャストでこの状況を回避するには、デバイスごとに固有の期間、同期アダプターの開始を延期することを検討してください。

次のコード スニペットは、受信した GCM メッセージに応じて requestSync() を実行する方法を示しています。

Kotlin

...
// Constants
// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Account type
const val ACCOUNT_TYPE = "com.example.android.datasync"
// Account
const val ACCOUNT = "default_account"
// Incoming Intent key for extended data
const val KEY_SYNC_REQUEST = "com.example.android.datasync.KEY_SYNC_REQUEST"
...
class GcmBroadcastReceiver : BroadcastReceiver() {
    ...
    override fun onReceive(context: Context, intent: Intent) {
        // Get a GCM object instance
        val gcm: GoogleCloudMessaging = GoogleCloudMessaging.getInstance(context)
        // Get the type of GCM message
        val messageType: String? = gcm.getMessageType(intent)
        /*
         * Test the message type and examine the message contents.
         * Since GCM is a general-purpose messaging system, you
         * may receive normal messages that don't require a sync
         * adapter run.
         * The following code tests for a a boolean flag indicating
         * that the message is requesting a transfer from the device.
         */
        if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE == messageType
            && intent.getBooleanExtra(KEY_SYNC_REQUEST, false)) {
            /*
             * Signal the framework to run your sync adapter. Assume that
             * app initialization has already created the account.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null)
            ...
        }
        ...
    }
    ...
}

Java

public class GcmBroadcastReceiver extends BroadcastReceiver {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Account type
    public static final String ACCOUNT_TYPE = "com.example.android.datasync";
    // Account
    public static final String ACCOUNT = "default_account";
    // Incoming Intent key for extended data
    public static final String KEY_SYNC_REQUEST =
            "com.example.android.datasync.KEY_SYNC_REQUEST";
    ...
    @Override
    public void onReceive(Context context, Intent intent) {
        // Get a GCM object instance
        GoogleCloudMessaging gcm =
                GoogleCloudMessaging.getInstance(context);
        // Get the type of GCM message
        String messageType = gcm.getMessageType(intent);
        /*
         * Test the message type and examine the message contents.
         * Since GCM is a general-purpose messaging system, you
         * may receive normal messages that don't require a sync
         * adapter run.
         * The following code tests for a a boolean flag indicating
         * that the message is requesting a transfer from the device.
         */
        if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)
            &&
            intent.getBooleanExtra(KEY_SYNC_REQUEST)) {
            /*
             * Signal the framework to run your sync adapter. Assume that
             * app initialization has already created the account.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null);
            ...
        }
        ...
    }
    ...
}

コンテンツ プロバイダのデータ変更時に同期アダプターを実行する

アプリがコンテンツ プロバイダでデータを収集し、プロバイダを更新するたびにサーバーを更新する場合は、同期アダプターを自動的に実行するようにアプリを設定できます。これを行うには、コンテンツ プロバイダのオブザーバーを登録します。コンテンツ プロバイダ内のデータが変更されると、コンテンツ プロバイダ フレームワークがオブザーバーを呼び出します。オブザーバーで requestSync() を呼び出して、同期アダプターを実行するようにフレームワークに指示します。

注: スタブ コンテンツ プロバイダを使用している場合は、コンテンツ プロバイダにデータがなく、onChange() は呼び出されません。この場合は、デバイスデータの変更を検出する独自のメカニズムを提供する必要があります。このメカニズムは、データが変更されたときに requestSync() を呼び出す役割も担います。

コンテンツ プロバイダのオブザーバーを作成するには、ContentObserver クラスを拡張して、両方の形式の onChange() メソッドを実装します。onChange() で、requestSync() を呼び出して同期アダプターを起動します。

オブザーバーを登録するには、registerContentObserver() の呼び出しで引数として渡します。この呼び出しでは、監視するデータのコンテンツ URI も渡す必要があります。コンテンツ プロバイダ フレームワークは、このスマートウォッチ URI を、プロバイダを変更する ContentResolver メソッド(ContentResolver.insert() など)に引数として渡されたコンテンツ URI と比較します。一致した場合は、ContentObserver.onChange() の実装が呼び出されます。

次のコード スニペットは、テーブルが変更されたときに requestSync() を呼び出す ContentObserver を定義する方法を示しています。

Kotlin

// Constants
// Content provider scheme
const val SCHEME = "content://"
// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Path for the content provider table
const val TABLE_PATH = "data_table"
...
class MainActivity : FragmentActivity() {
    ...
    // A content URI for the content provider's data table
    private lateinit var uri: Uri
    // A content resolver for accessing the provider
    private lateinit var mResolver: ContentResolver
    ...
    inner class TableObserver(...) : ContentObserver(...) {
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         * This method signature is provided for compatibility with
         * older platforms.
         */
        override fun onChange(selfChange: Boolean) {
            /*
             * Invoke the method signature available as of
             * Android platform version 4.1, with a null URI.
             */
            onChange(selfChange, null)
        }

        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         */
        override fun onChange(selfChange: Boolean, changeUri: Uri?) {
            /*
             * Ask the framework to run your sync adapter.
             * To maintain backward compatibility, assume that
             * changeUri is null.
             */
            ContentResolver.requestSync(account, AUTHORITY, null)
        }
        ...
    }
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Get the content resolver object for your app
        mResolver = contentResolver
        // Construct a URI that points to the content provider data table
        uri = Uri.Builder()
                .scheme(SCHEME)
                .authority(AUTHORITY)
                .path(TABLE_PATH)
                .build()
        /*
         * Create a content observer object.
         * Its code does not mutate the provider, so set
         * selfChange to "false"
         */
        val observer = TableObserver(false)
        /*
         * Register the observer for the data table. The table's path
         * and any of its subpaths trigger the observer.
         */
        mResolver.registerContentObserver(uri, true, observer)
        ...
    }
    ...
}

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider scheme
    public static final String SCHEME = "content://";
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Path for the content provider table
    public static final String TABLE_PATH = "data_table";
    // Account
    public static final String ACCOUNT = "default_account";
    // Global variables
    // A content URI for the content provider's data table
    Uri uri;
    // A content resolver for accessing the provider
    ContentResolver mResolver;
    ...
    public class TableObserver extends ContentObserver {
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         * This method signature is provided for compatibility with
         * older platforms.
         */
        @Override
        public void onChange(boolean selfChange) {
            /*
             * Invoke the method signature available as of
             * Android platform version 4.1, with a null URI.
             */
            onChange(selfChange, null);
        }
        /*
         * Define a method that's called when data in the
         * observed content provider changes.
         */
        @Override
        public void onChange(boolean selfChange, Uri changeUri) {
            /*
             * Ask the framework to run your sync adapter.
             * To maintain backward compatibility, assume that
             * changeUri is null.
             */
            ContentResolver.requestSync(mAccount, AUTHORITY, null);
        }
        ...
    }
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Get the content resolver object for your app
        mResolver = getContentResolver();
        // Construct a URI that points to the content provider data table
        uri = new Uri.Builder()
                  .scheme(SCHEME)
                  .authority(AUTHORITY)
                  .path(TABLE_PATH)
                  .build();
        /*
         * Create a content observer object.
         * Its code does not mutate the provider, so set
         * selfChange to "false"
         */
        TableObserver observer = new TableObserver(false);
        /*
         * Register the observer for the data table. The table's path
         * and any of its subpaths trigger the observer.
         */
        mResolver.registerContentObserver(uri, true, observer);
        ...
    }
    ...
}

同期アダプターを定期的に実行する

同期アダプターを定期的に実行するには、実行と実行の間の待機時間を設定するか、特定の時間帯に実行するか、またはその両方を行います。同期アダプターを定期的に実行すると、サーバーの更新間隔とほぼ一致させることができます。

同様に、同期アダプターを夜間に実行するよう設定することで、サーバーが比較的アイドル状態のときにデバイスからデータをアップロードできます。ほとんどのユーザーは夜間に電源を入れて電源に接続したままにしているため、通常この時間を確保できます。また、デバイスは同期アダプターと同時に他のタスクを実行しません。ただし、この方法を使用する場合は、各デバイスがデータ転送をトリガーするタイミングを若干変える必要があります。すべてのデバイスで同期アダプターが同時に実行されると、サーバーと携帯通信会社のデータ ネットワークが過負荷になる可能性があります。

一般に、ユーザーが即時アップデートを必要とせず、定期的なアップデートを希望している場合は、定期的な実行が適しています。また、最新データの可用性と、デバイス リソースを過剰に消費しない小規模な同期アダプターの実行効率のバランスを保ちたい場合にも役立ちます。

同期アダプターを定期的に実行するには、addPeriodicSync() を呼び出します。これにより、一定の時間が経過した後に同期アダプターが実行されるようにスケジュールされます。同期アダプター フレームワークは他の同期アダプターの実行を考慮し、バッテリーの効率を最大限に高めようとするため、経過時間が数秒異なる場合があります。また、ネットワークが利用できない場合、フレームワークは同期アダプターを実行しません。

addPeriodicSync() は、特定の時刻に同期アダプターを実行しません。毎日ほぼ同じ時間に同期アダプターを実行するには、繰り返しアラームをトリガーとして使用します。繰り返しアラームについて詳しくは、AlarmManager のリファレンス ドキュメントをご覧ください。メソッド setInexactRepeating() を使用して変動する時間帯トリガーを設定する場合は、開始時間をランダム化して、異なるデバイスからの同期アダプターの実行がずらされるようにする必要があります。

addPeriodicSync() メソッドによって setSyncAutomatically() が無効にならないため、比較的短時間で複数の同期が実行される場合があります。また、addPeriodicSync() の呼び出しで使用できる同期アダプターの制御フラグは数個のみです。使用できないフラグについては、addPeriodicSync() のリファレンス ドキュメントをご覧ください。

次のコード スニペットは、同期アダプターの定期的な実行をスケジュールする方法を示しています。

Kotlin

// Content provider authority
const val AUTHORITY = "com.example.android.datasync.provider"
// Account
const val ACCOUNT = "default_account"
// Sync interval constants
const val SECONDS_PER_MINUTE = 60L
const val SYNC_INTERVAL_IN_MINUTES = 60L
const val SYNC_INTERVAL = SYNC_INTERVAL_IN_MINUTES * SECONDS_PER_MINUTE
...
class MainActivity : FragmentActivity() {
    ...
    // A content resolver for accessing the provider
    private lateinit var mResolver: ContentResolver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Get the content resolver for your app
        mResolver = contentResolver
        /*
         * Turn on periodic syncing
         */
        ContentResolver.addPeriodicSync(
                mAccount,
                AUTHORITY,
                Bundle.EMPTY,
                SYNC_INTERVAL)
        ...
    }
    ...
}

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY = "com.example.android.datasync.provider";
    // Account
    public static final String ACCOUNT = "default_account";
    // Sync interval constants
    public static final long SECONDS_PER_MINUTE = 60L;
    public static final long SYNC_INTERVAL_IN_MINUTES = 60L;
    public static final long SYNC_INTERVAL =
            SYNC_INTERVAL_IN_MINUTES *
            SECONDS_PER_MINUTE;
    // Global variables
    // A content resolver for accessing the provider
    ContentResolver mResolver;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Get the content resolver for your app
        mResolver = getContentResolver();
        /*
         * Turn on periodic syncing
         */
        ContentResolver.addPeriodicSync(
                mAccount,
                AUTHORITY,
                Bundle.EMPTY,
                SYNC_INTERVAL);
        ...
    }
    ...
}

オンデマンドで同期アダプターを実行する

ユーザー リクエストに応じて同期アダプターを実行することは、同期アダプターを実行する方法として最も適していません。このフレームワークは、スケジュールに従って同期アダプターを実行する際にバッテリーを節約するように特別に設計されています。データの変更に応じて同期を実行するオプションでは、新しいデータの提供に電力が使われるため、バッテリーを効率的に使用します。

それに対して、ユーザーがオンデマンドで同期を実行できるようにすると、同期が単独で実行されるため、ネットワークと電源リソースを非効率的に使用することになります。また、オンデマンド同期を設定すると、データが変更されたという証拠がない場合でも、ユーザーは同期をリクエストすることになります。また、データを更新しない同期の実行は、バッテリー電力の無駄な使い道になります。一般的に、アプリは他のシグナルを使用して同期をトリガーするか、ユーザー入力なしで定期的に同期のスケジュールを設定する必要があります。

それでも同期アダプターをオンデマンドで実行する必要がある場合は、同期アダプターの手動実行用に同期アダプター フラグを設定し、ContentResolver.requestSync() を呼び出します。

次のフラグを指定してオンデマンド転送を実行します。

SYNC_EXTRAS_MANUAL
強制的に手動で同期します。同期アダプター フレームワークは、setSyncAutomatically() によって設定されたフラグなどの既存の設定を無視します。
SYNC_EXTRAS_EXPEDITED
同期を強制的にすぐに開始します。設定しない場合、システムは短時間に多数のリクエストをスケジュールしてバッテリー使用量の最適化を試みるため、数秒待ってから同期リクエストを実行する可能性があります。

次のコード スニペットは、ボタンのクリックに応じて requestSync() を呼び出す方法を示しています。

Kotlin

// Constants
// Content provider authority
val AUTHORITY = "com.example.android.datasync.provider"
// Account type
val ACCOUNT_TYPE = "com.example.android.datasync"
// Account
val ACCOUNT = "default_account"
...
class MainActivity : FragmentActivity() {
    ...
    // Instance fields
    private lateinit var mAccount: Account
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        /*
         * Create the placeholder account. The code for CreateSyncAccount
         * is listed in the lesson Creating a Sync Adapter
         */

        mAccount = createSyncAccount()
        ...
    }

    /**
     * Respond to a button click by calling requestSync(). This is an
     * asynchronous operation.
     *
     * This method is attached to the refresh button in the layout
     * XML file
     *
     * @param v The View associated with the method call,
     * in this case a Button
     */
    fun onRefreshButtonClick(v: View) {
        // Pass the settings flags by inserting them in a bundle
        val settingsBundle = Bundle().apply {
            putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
            putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true)
        }
        /*
         * Request the sync for the default account, authority, and
         * manual sync settings
         */
        ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle)
    }

Java

public class MainActivity extends FragmentActivity {
    ...
    // Constants
    // Content provider authority
    public static final String AUTHORITY =
            "com.example.android.datasync.provider";
    // Account type
    public static final String ACCOUNT_TYPE = "com.example.android.datasync";
    // Account
    public static final String ACCOUNT = "default_account";
    // Instance fields
    Account mAccount;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        /*
         * Create the placeholder account. The code for CreateSyncAccount
         * is listed in the lesson Creating a Sync Adapter
         */

        mAccount = CreateSyncAccount(this);
        ...
    }
    /**
     * Respond to a button click by calling requestSync(). This is an
     * asynchronous operation.
     *
     * This method is attached to the refresh button in the layout
     * XML file
     *
     * @param v The View associated with the method call,
     * in this case a Button
     */
    public void onRefreshButtonClick(View v) {
        // Pass the settings flags by inserting them in a bundle
        Bundle settingsBundle = new Bundle();
        settingsBundle.putBoolean(
                ContentResolver.SYNC_EXTRAS_MANUAL, true);
        settingsBundle.putBoolean(
                ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        /*
         * Request the sync for the default account, authority, and
         * manual sync settings
         */
        ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
    }