注: バックグラウンド処理ユースケースのほとんどで、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.insert()
などのプロバイダを変更する ContentResolver
メソッドに引数として渡されるコンテンツ 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); ... } ... }
同期アダプターを定期的に実行する
同期アダプターを定期的に実行するには、実行から次の実行までの待ち時間を設定するか、1 日の特定の時間にそのアダプターを実行するか、またはその両方を行います。同期アダプターを定期的に実行すると、サーバーの更新間隔とほぼ一致させることができます。
同様に、同期アダプターが夜間に実行されるようにスケジュール設定することで、サーバーが比較的アイドル状態のときにデバイスからデータをアップロードできます。夜間も電源を入れて接続したままにしておくユーザーがほとんどであるため、通常はこの時間を利用できます。また、同期アダプター実行時にデバイスで他のタスクが実行されている可能性も低くなります。ただし、このアプローチを使用する場合は、データ転送がトリガーされるタイミングをデバイスごとに少しずつ確実にずらす必要があります。すべてのデバイスで同期アダプターが同時に実行されると、サーバーとモバイル プロバイダのデータ ネットワークが過負荷になる可能性があります。
一般的に、定期的な実行はユーザーがインスタント アップデートを必要とせず、定期的なアップデートを求めている場合に適しています。また、最新データの入手と、デバイス リソースが過剰に使用されない小さな同期アダプターの効率性のバランスを取る必要がある場合にも有効です。
同期アダプターを定期的に実行するには、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 dummy 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 dummy 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); }