参考:Content Provider Basics | Android Developers
参考:Transferring Data Using Sync Adapter | Android Developers
参考:ContentResolver | Android Developers
参考:ContentProviderOperation | Android Developers
参考:AbstractThreadedSyncAdapter | Android Developers
目次
- ContentProvider へのアクセス
- バッチ処理
- Intent によるアクセス
- データの同期
- ContentProvider を用いた同期の仕組み
- 同期の仕組みを使用する準備
- AbstractThreadedSyncAdapter
- Service
- メタデータの宣言
- AndroidManifest の宣言
- 同期の実行と下位互換
ContentProvider へのアクセス
ContentProvider へのアクセスは、ContentResolver を介して行うことは、2.10. データベースにて解説しました。このページの方法では、1 回のアクセスで 1 回の操作(挿入、更新、削除)を行うように動作します。
今回は、1 回のアクセスで複数の操作(挿入、更新、削除)を行う、バッチ処理について解説するとともに、ContentResolver 以外の手段を用いた ContentProvider へのアクセスについて解説します。
バッチ処理
ContentProviderOperation に、挿入・更新・削除のいずれかの処理のための情報をもたせ、このオブジェクトを List にまとめて ContentResolver 経由でリクエストすることで、バッチ処理が実行できます。
バッチ処理と同等のリクエストを複数回繰り返すよりも、バッチ処理の方が高速に動作します。一方で、バッチ処理はそのままではアトミック性を保証しないまま動作してしまうため、必要であればContentProvider#applyBatch(ArrayList)
をオーバライドして、トランザクションを自分で開始・終了する必要があります。
以下に、挿入・更新・削除のそれぞれの処理を構成する ContentProviderOperation の作成例と、バッチ処理の適用方法を示します。
public void batchInsert(Context context, List<String> names) {
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// List に ContentProviderOperation を詰め込んで、バッチ処理時にすべてを実行するようにする
for (String name : names) {
ContentValues values = new ContentValues();
values.put("name", name);
// ContentProviderOperation#newInsert(Uri) によって、挿入に関する操作手順を生成するビルダーを取得する
ContentProviderOperation insertOp = ContentProviderOperation.newInsert(MyContentProvider.CONTENT_URI)
.withValues(values) // ContentValues に含まれるデータを挿入
.build(); // ContentProviderOperation を作成
ops.add(insertOp);
}
ContentResolver resolver = context.getContentResolver();
resolver.applyBatch(MyContentProvider.AUTHORITY, ops); // バッチ処理を実行
}
public void batchUpdate(Context context, List<Integer> ids, List<String> names) {
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
for (int i = 0; i < ids.size() && i < names.size(); i++) {
Integer id = ids.get(i);
String name = names.get(i);
ContentValues values = new ContentValues();
values.put("name", name);
// ContentProviderOperation#newUpdate(Uri) によって、更新に関する操作手順を生成するビルダーを取得する
ContentProviderOperation updateOp = ContentProviderOperation.newUpdate(MyContentProvider.CONTENT_URI)
.withValues(values)
.withSelection("_id = ?", new String[] { id.toString() }) // Update 対象の条件を付与
.build();
ops.add(updateOp);
}
ContentResolver resolver = context.getContentResolver();
resolver.applyBatch(MyContentProvider.AUTHORITY, ops); // バッチ処理を実行
}
public void batchDelete(Context context, List<Integer> ids) {
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
for (Integer id : ids) {
// ContentProviderOperation#newUpdate(Uri) によって、削除に関する操作手順を生成するビルダーを取得する
ContentProviderOperation deleteOp = ContentProviderOperation.newDelete(MyContentProvider.CONTENT_URI)
.withSelection("_id = ?", new String[] { id.toString() })
.build();
ops.add(deleteOp);
}
ContentResolver resolver = context.getContentResolver();
resolver.applyBatch(MyContentProvider.AUTHORITY, ops);
}
挿入・更新・削除とも、操作に合わせて呼び出すメソッドが異なりますが、それぞれで得られるオブジェクトの型はすべて ContentProviderOperation.Builder となります。操作に応じて、 ContentProviderOperation.Builder の各種メソッドで手順の設定の仕方が変わることに注意して下さい。例えば、ContentProviderOperation#newInsert(Uri)
で生成した ContentProviderOperation.Builder では、ContentProviderOperation.Builder#withSelection(String, String[])
が使用できず、実行時にIllegalArgumentException
となります。
挿入・更新・削除の操作を複数含む ArrayList を生成すれば、バッチ処理中にそれらの手順をまとめて実行することもできます。
バッチ処理の実行は、ContentResolver#applyBatch(String, ArrayList)
によって行い、返り値として、処理結果を表すContentProviderResult
の配列が得られます。
Intent によるアクセス
データの同期
しばしば、アプリケーションはサーバサイドとのデータのやり取りを密接にやりとりします。ネットワークにつながらない場所に居ても、最低限、それ以前にサーバからダウンロードしておいたデータへのアクセス性を確保したり、定期的にアプリケーション内に蓄積したデータをサーバへ送信することでバックアップを取ったりするような仕組みは、ネットワークへの接続が必ずしも常にあるとは限らない環境において、また、多様な端末を複数使用する環境においては、とても大切な仕組みとなります。
Android では、これらを支援する仕組みとして、クラウドとの同期のためのフレームワークを提供しています。今回は、特に ContentProvider を用いたフレームワークに注目して解説します。
ContentProvider 以外にも同期の仕組みが存在しますが、この仕組は3.08. クラウド同期で解説します。
ContentProvider を用いた同期の仕組み
API Level 8 から、ContentProvider の保持しているデータを、定期的にサーバ(クラウド)と同期する仕組みが導入されました。端末の AccountManager で管理されているアカウントに紐付いて、同期の定期実行をシステムに促すことで、指定した間隔で同期を実行するようになります。
複数の ContentProvider を持つ場合、それぞれの ContentProvider で個別に同期処理を実行させることができます。この時、それぞれの同期処理は並列には実行されず、キューに積まれて順次同期処理が実行されます。このため、他の同期処理の影響を受けて、指定した間隔通りに同期処理が実行されないことがある点に注意して下さい。
同期の仕組みを使用する準備
AccountManager で管理されているアカウントが必要となるため、ContentProvider を用いたデータの同期をするには、Authenticator の使用が必要となります。既に Authenticator によるアカウント管理の仕組みがある場合は特に作業の必要はありませんが、アカウント管理の仕組みが必要でないアプリケーションでは、スタブの Authenticator を用意する必要があります。
以下がスタブの Authenticator の実装の例です。アカウント管理をしないのであれば、ロジックを記述せず、操作がサポートされない旨を表すUnsupportedOperationException
をスローするようにします。
public class StubAuthenticator extends AbstractAccountAuthenticator {
public StubAuthenticator(Context context) {
super(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
return null; // アカウント管理をしないため、null を返す
}
@Override
public abstract Bundle confirmCredentials (AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null; // アカウント生成の了承リクエストは無視するため、null を返す
}
@Override
public abstract Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
throw new UnsupportedOperationException("edit properties not supported"); // アカウント情報の編集は、アカウント管理をしないポリシーとなるので例外とする
}
@Override
public abstract Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetowrkErrorException {
throw new UnsupportedOperationException("get auth token not supported"); // トークン管理も、アカウント管理をしないため例外
}
@Override
public abstract String getAuthTokenLabel(String authTokenType) {
throw new UnsupportedOperationException("get auth token label not supported"); // トークン管理をしないので、トークンの種類に対応する表示も例外
}
@Override
public abstract Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
throw new UnsupportedOperationException("feature check not supported"); // アカウント管理をしないため例外
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) throws NetworkErrorException {
throw new UnsupportedOperationException(); // アカウント管理をしないため例外
}
}
Authenticator の実装ができたら、Authenticator を動作させるための Service を実装します。
public class StubAuthenticatorService extends Service {
// スタブの Authenticator を保持しておく
private StubAuthenticator mAuthenticator;
@Override
public void onCreate() {
mAuthenticator = new StubAuthenticator(this);
}
// システムは、Authenticator に対して RPC を実施する為にこの Service にバインドするので、RPC のための IBinder を Authenticator から取得する
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}
次に、スタブの Authenticator に関するメタデータの宣言をします。
属性名 | 意味 |
---|---|
android:accountType | アカウントの識別子。同期処理をシステムが管理する際にも、どのアカウントに紐づくものかを識別するために使用する。 |
android:icon | アカウントのアイコン。同期処理をユーザによって設定可能にする場合は必須。 |
android:smallIcon | スクリーンの大きさによって、icon 属性に指定した画像のかわりに使用される。 |
android:label | アカウントの表示名。同期処理をユーザによって設定可能にする場合は必須。 |
<!-- res/xml/authenticator.xml -->
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="jp.co.mixi.sample.sync.stub.account"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/app_name"/>
AndroidManifest には Service と同期処理を紐付ける ContentProvider を宣言します。
Service の宣言には、<intent-filter>
と<meta-data>
を含める必要があります。
ContentProvider の宣言では、android:syncable
属性をtrue
にしておく必要があります。
<service
android:name="jp.mixi.sample.sync.stub.authenticator.StubAuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<provider
android:name="jp.mixi.sample.sync.StubSyncProvider"
android:authorities="jp.mixi.sample.sync.StubSyncProvider"
android:export="false"
android:syncable="true"/>
最後に、端末にアカウント情報を登録します。
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstancestate);
setContentView(R.layout.activity_main);
// ...
Account account = createAccount();
}
private Account createAccount() {
AccountManager manager = (AccountManager) getSystemService(Context.ACCOUNT_SERVICE);
// スタブのアカウント情報を持つ Account オブジェクトを作る
Account stubAccount = new Account("stubAccount", "jp.co.mixi.sample.sync.stub.account");
// AccountManager にスタブのアカウント情報を登録
if (manager.addAccountExplicitly(stubAccount, null, null)) {
// 登録成功
} else {
// 既に登録されているか、アカウントが null、その他エラーの場合
}
return stubAccount;
}
}
AbstractThreadedSyncAdapter
ContentProvider のデータの同期を目的としたクラスで、ContentResolver によって同期のタイミングが管理されます。
このクラスは、アプリケーションプロセスが実行中、ワーカスレッド上でクラウドとの同期処理を記述するフレームワークを提供しています。UI スレッドとは別のスレッド上で実行される為、各種の IO を同期的に記述しても問題ありません。
public MySyncAdapter extends AbstractThreadedSyncAdapter {
public MySyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
/**
* 定期実行処理が走った時に呼ばれるコールバック
*
* @param account 定期実行のリクエストが紐付けられているアカウント
* @param extras 定期実行をシステムに登録した時に渡すパラメータを含むマップオブジェクト
* @param authority ContentProvider の AUTHORITY
* @param provider ContentProvider へのアクセサ
* @param syncResult 同期処理の結果を保持するオブジェクト。成功・例外などの情報を持つ
*/
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
// ここに同期のための処理を記述する
}
}
Service
データの同期のタイミングで呼び出され、AbstractThreadedSyncAdapter に記述した同期の手続きを実行するための Service を定義します。
AbstractThreadedSyncAdapter のインスタンスは、アプリケーションのライフサイクルの中で Singleton として扱うようにし、複数の AbstractThreadedSyncAdapter のインスタンスを生成して並列に同期を実行してしまうことを防ぎます。
public MySyncService extends Service {
private static AbstractThreadedSyncAdapter sAdapter;
private static final Object LOCK = new Object();
@Override
public void onCreate() {
synchronized (LOCK) {
if (sAdapter == null) {
sAdapter = new MySyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sAdapter.getSyncAdapterBinder();
}
}
メタデータの宣言
AbstractThreadedSyncAdapter のためのメタデータを下記のように宣言します。
属性名 | 意味 |
---|---|
android:contentAuthority | ContentProvider の AUTHORITY を指定する。 |
android:accountType | Authenticator のメタデータに記述したアカウントの識別子を指定する。 |
android:userVisible | 設定画面でユーザがこの同期処理をコントロール出来るようにするかどうか。 |
android:supportsUploading | 同期中、データのアップロードをするかどうか。ダウンロードのみの場合はfalse とする。 |
android:allowParallelSyncs | 複数の AbstractThreadedSyncAdapter のインスタンスから同時並行に同期処理を実行することを許すかどうか。複数アカウントを同じアプリケーションが管理する際に使用するため、単一のアカウントしかありえない場合はfalse とする。 |
android:isAlwaysSyncable | 常に同期できるようにしておくかどうか。手動で設定出来るようにする場合はfalse とする。 |
<!-- res/xml/syncadapter.xml -->
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter
xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="jp.mixi.sample.sync.StubSyncProvider"
android:accountType="jp.co.mixi.sample.sync.stub.account"
android:userVisible="false"
android:supportsUploading="false"
android:allowParallelSyncs="false"
android:isAlwaysSyncable="true"/>
AndroidManifest の宣言
定期的な同期の実行は、Android が提供するフレームワークに則って実装し、自動的にフレームワークによって管理・実行されます。このため、定期的な同期の実行の仕組みを持つアプリケーションは、パーミッションの使用を宣言する必要があります。
<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"/>
次に、AbstractThreadedSyncAdapter を動かす Service を宣言します。この Service はシステムによって呼び出される為、android:exported
をtrue
にしておきます。
<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>
同期の実行と下位互換
これまでは、同期に必要なコンポーネントの作り方を見てきましたが、これらを準備しただけでは同期が実行されるようにはなりません。
アプリケーションの初回実行時に、システムに登録する必要があります。
定期実行する同期処理の登録には、ContentResolver を使用します。また、AccountManager に登録した Account に紐付けるため、Account も必要です。
指定した間隔で同期する
指定した間隔で同期するようシステムに登録するには、ContentResolver#addPeriodicSync(Account, String, Bundle, long)
を使用します。
/**
* @param account 紐付けるアカウント
* @param frequencySec 同期処理を実行する間隔(秒)
*/
public void applySyncPeriod(Account account, long frequencySec) {
Bundle args = new Bundle(); // 同期処理の制御に関するパラメータを保持するマップオブジェクト
//何らか同期処理のために渡したいパラメータがある場合はこの Bundle に詰め込む
ContentResolver.addPeriodicSync(account, StubContentProvider.AUTHORITY, args, frequencySec);
}
ネットワークの状態に応じて同期する
Android システムは、ネットワークの接続が確立されると、TCP/IP のコネクションを開いたままの状態に保つために、短い間隔でメッセージを創出し続けます。このメッセージの送出に合わせて同期する場合は、ContentResolver#setSyncAutomatically(Account, String, boolean)
を使用して登録します。
/**
* @param account 紐付けるアカウント
*/
public void applySyncPeriod(Account account) {
// 最後の boolean を true にすれば同期が有効となり、false にすれば同期が無効化される
ContentResolver.setSyncAutomatically(account, StubContentProvider.AUTHORITY, true);
}
短時間で頻繁に同期が実行されるようになりますが、この操作によって、ContentResolver#addPeriodicSync(Account, String, Bundle, long)
によって登録された同期処理が無効化されるわけではありません。一定の間隔で実行すればよいものは、ContentResolver#addPeriodicSync(Account, String, Bundle, long)
を用いるだけで事足ります。
オンデマンドに同期する
ボタンをおした時など、何かしらのイベントをトリガに同期を手動で実行したい時には、ContentResolver#requestSync(Account, String, Bundle)
を使用します。
フレームワークは、定期的に実行するタイミングで効率よく処理を行えるようチューニングされているため、オンデマンドに同期処理を起動することは推奨されていません。また、データの同期が必要ない場合でも無理やり同期を実行させてしまうため、無駄にシステムのリソースを消費してしまう可能性があります。これらの理由から、オンデマンドに同期する機能の実装は推奨されていません。
public void dispatchSync(Account account) {
Bundle args = new Bundle();
args.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // 手動で同期するフラグをシステムに伝える
args.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); // キューの先頭に積み、すぐに同期を実行するようシステムに伝える
ContentResolver.requestSync(account, StubContentProvider.AUTHORITY, args);
}
下位互換性
ContentResolver が持つ各種の定期的な同期のためのメソッドは、API Level 8 から導入されました。一方で、AbstractThreadedSyncAdapter など、同期に使われるコンポーネントは API Level 5 から導入されています。このため、定期的に実行するようシステムに伝える部分を、API Level 7 以前の OS の為に自分で実装する必要があります。
単純な、一定間隔での同期の実行であれば、AlarmManager
を用いて対応することができます。
/**
* @param account 紐付けるアカウント
* @param frequencySec 同期処理を実行する間隔(秒)
*/
public void applySyncPeriod(Context context, Account account, long frequencySec) {
Bundle args = new Bundle(); // 同期処理の制御に関するパラメータを保持するマップオブジェクト
//何らか同期処理のために渡したいパラメータがある場合はこの Bundle に詰め込む
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
// AlarmManager ではミリ秒で管理されるのでミリ秒へ変換
long frequencyMillis = frequencySec * 1000;
// AlarmManager によって、定期的にブロードキャストされる Intent を準備する
Intent intent = new Intent(SyncTriggerReceiver.ACTION_TRIGGER, Uri.parse("alarm:" + authority));
intent.putExtra(SyncTriggerReceiver.EXTRA_SYNC_AUTHORITY, StubContentProvider.AUTHORITY);
intent.putExtra(SyncTriggerReceiver.EXTRA_SYNC_ACCOUNT_NAME, account.name);
intent.putExtra(SyncTriggerReceiver.EXTRA_SYNC_ACCOUNT_TYPE, account.type);
intent.putExtra(SyncTriggerReceiver.EXTRA_SYNC_EXTRAS, extras);
// AlarmManager によって Intent のハンドリングがなされるため、遅延させるために PendingIntent を作成
PendingIntent pendingOperation = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// AlarmManager に登録
AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + frequencyMillis, frequencyMillis, operation);
} else {
ContentResolver.addPeriodicSync(account, StubContentProvider.AUTHORITY, args, frequencySec);
}
}
これに対応して、BroadcastReceiver を準備します。
public class SyncTriggerReceiver extends BroadcastReceiver {
public static final String ACTION_TRIGGER = "ACTION_TRIGGER";
public static final String EXTRA_SYNC_AUTHORITY = "authority";
public static final String EXTRA_SYNC_ACCOUNT_NAME = "account_name";
public static final String EXTRA_SYNC_ACCOUNT_TYPE = "account_type";
public static final String EXTRA_SYNC_EXTRAS = "extras";
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_TRIGGER.equals(intent.getAction)) {
String accountName = intent.getStringExtra(EXTRA_SYNC_ACCOUNT_NAME);
String accountType = intent.getStringExtra(EXTRA_SYNC_ACCOUNT_TYPE);
String authority = intent.getStringExtra(EXTRA_SYNC_AUTHORITY);
Bundle extras = intent.getParcelableExtra(EXTRA_SYNC_EXTRAS);
Account account = new Account(accountName, accountType);
ContentResolver.requestSync(account, authority, extras);
}
}
}