明日の自分のために

moon indicating dark mode
sun indicating light mode

ContentProviderでSharedPreferencesを共有する

August 14, 2020

少量のkey-valueデータをアプリ間で共有する方法を考える

ContentProviderはデータへのアクセスを管理する仕組みです。データをどの様に保存するかは選ぶことができます。
Androidにおけるデータの保存方法はファイル、SQLite、クラウド、SharedPreferenceなどがあります。

ここではkey-valueデータのような単純かつ少量(例えば1つだけ)のデータを共有する場合を考えます。

1つのkey-valueデータならSharedPreferenceに保存したいところですが、SharedPreferenceはアプリ間で共有できません。 (APIレベル17まではSharedPreferenceにMODE_WORLD_READABLEMODE_WORLD_WRITEABLEがありましたが、今は使えません。) また、複数のプロセスからのアクセスをサポートしていません。

データを共有する方法としては、データベースを作成してkey-valueデータを保存し、ContentProvider経由でアクセスする方法がまず思いつきます。
おそらくこの方法で問題ないでしょう。ただ、1つのkey-valueデータを共有するためだけにテーブルを作成してCRUD機能を提供するのは、いささか大げさな気もします。

そこで今回は、SharedPreferenceにkey-valueデータを保存しContentProvider.call()を使ってデータへのアクセス方法を提供することで、簡単にデータの共有が実現できないか試してみました。
ContentProvider.callを使うことで、providerが用意したget / put メソッドを呼び出してデータにアクセスできるようにするということです。

ContentProvider.call()

ContentProvider.call()を実装することでContentProviderが定義するメソッドを呼び出す事ができます。

以下のように、ContentProvider側で受け取ったメソッド名と引数から、対応するメソッドを呼び出して、Bundleに実行結果を入れて返却するようにContentProvider.call()を実装します。
これを使うとaidlを自分で定義するなどプロセス間通信のための作業なく、プロセスをまたいだメソッド呼び出しを実現することができます。

override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
val bundle = Bundle()
when (method) {
MyPreferences.GET_STRING_PREFERENCE_METHOD -> {
Log.d(TAG, "get")
val str: String = getStringPreference()
bundle.putString(MyPreferences.SAVED_STRING_KEY, str)
}
MyPreferences.PUT_STRING_PREFERENCE_METHOD -> {
Log.d(TAG, "put")
putStringPreference(arg ?: "")
}
}
return bundle
}

ContentProvider.call()の中でSharedPreferencesにアクセスします。

private lateinit var prefs: SharedPreferences
private fun getStringPreference(): String {
return prefs.getString(
MyPreferences.SAVED_STRING_KEY,
"default value"
) ?: ""
}
private fun putStringPreference(str: String) {
with(prefs.edit()) {
putString(
MyPreferences.SAVED_STRING_KEY,
str
)
commit()
}
}

今回は使用しませんが、作成するContentProviderではinsert(), query(), update(), delete(), getType()をオーバーライドしておく必要があります。

それとContentProviderを提供するアプリの、AndroidManifest.xmlの<application>タグ内に<provider>タグを追記するのを忘れないようにして下さい。

<provider
android:name=".provider.MyContentProvider"
android:authorities="gan0803.pj.sharedpreferencestudy.provider"
android:enabled="true"
android:exported="true" />

ContentResolver.call()

ContentProviderのクライアント側はContentResolver.callを使います。

val uri = Uri.parse(MyPreferences.URI);
val cr = contentResolver
val auth = MyPreferences.AUTHORITY
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cr.call(auth, MyPreferences.READ_PREFERENCE_METHOD, null, null)
cr.call(auth, MyPreferences.PUT_STRING_PREFERENCE_METHOD, System.currentTimeMillis().toString(), null)
val bundle = cr.call(auth, MyPreferences.GET_STRING_PREFERENCE_METHOD, null, null)
val str: String = bundle?.getString(MyPreferences.SAVED_STRING_KEY) ?: ""
Log.d(TAG, "get: $str")
} else {
cr.call(uri, MyPreferences.PUT_STRING_PREFERENCE_METHOD, System.currentTimeMillis().toString(), null)
cr.call(uri, MyPreferences.READ_PREFERENCE_METHOD, null, null)
val bundle = cr.call(uri, MyPreferences.GET_STRING_PREFERENCE_METHOD, null, null)
val str: String = bundle?.getString(MyPreferences.SAVED_STRING_KEY) ?: ""
Log.d(TAG, "get: $str")
}

ContentProviderの利用にあたってはURIやメソッド名などの決め事を守る必要があります。
この決め事はコントラクトクラスにまとめてアプリ間で共有します。
今回使用するURIやメソッド名などの決め事をまとめたコントラクトクラスは以下の通りです。

class MyPreferences {
companion object {
public const val SAVED_STRING_KEY = "SavedStringKey"
public const val SAVED_BOOLEAN_KEY = "SavedBooleanKey"
public const val SAVED_INT_KEY = "SavedIntKey"
public const val URI = "content://gan0803.pj.sharedpreferencestudy.provider/"
public const val AUTHORITY = "gan0803.pj.sharedpreferencestudy.provider"
public const val GET_STRING_PREFERENCE_METHOD = "getStringPreference"
public const val PUT_STRING_PREFERENCE_METHOD = "putStringPreference"
public const val SAVE_PREFERENCE_METHOD = "savePreferences"
public const val READ_PREFERENCE_METHOD = "readPreferences"
}
}

permissionの設定

ContentProviderでは必要に応じて独自のpermissionを設定することができます。 詳細はパーミッションを実装するを確認して下さい。

AndroidManifest.xmlでpermissionを定義してproviderで利用します。
<manifest>タグ内に以下の通りpermissionを追加します。

<permission
android:name="gan0803.pj.sharedpreferencestudy.permission.PROVIDER"
android:protectionLevel="normal"/>

permissionについては以下を参照して下さい。
https://developer.android.com/guide/topics/manifest/permission-element?hl=ja

<provider>タグのandroid:permissionに定義した上記のpermissionを指定します。

<provider
android:name=".provider.MyContentProvider"
android:authorities="gan0803.pj.sharedpreferencestudy.provider"
android:permission="gan0803.pj.sharedpreferencestudy.permission.PROVIDER"
android:enabled="true"
android:exported="true" />

providerには以下のようなパーミッションがあります。

  • android:grantUriPermssions:一時的なパーミッションのフラグです。
  • android:permission:1 つのプロバイダ全体の読み取り / 書き込みパーミッションです。
  • android:readPermission:プロバイダ全体の読み取りパーミッションです。
  • android:writePermission:プロバイダ全体の書き込みパーミッションです。

SharedPreferences

SharedPreferencesについては、Key-Value データを保存するに説明があります。

今回は複雑なケースを想定していないので、getSharedPreferences, getPreferences, getDefaultSharedPreferencesどれを使っても良いと思います。

以下のように使います。

private var prefs: SharedPreferences
constructor(activity: Activity) {
prefs = activity.getSharedPreferences("savedPreferences", Context.MODE_PRIVATE)
}
fun savePreferences() {
with(prefs.edit()) {
putString(
SAVED_STRING_LOCAL_KEY,
"test local string"
)
putBoolean(
SAVED_BOOLEAN_LOCAL_KEY,
false
)
putInt(
SAVED_INT_LOCAL_KEY,
987654321
)
commit()
}
}

サンプルアプリのソースコード

今回、調べつつ試行錯誤して作成したサンプルアプリのソースコードは以下のリポジトリにあります。

サンプルではMyPreferencesを単純にコピーしていますが、実際はデータ提供側がライブラリ化して利用者から使えるようにしておくべきでしょう。
Managerクラスも作れば完全にメソッド呼び出しで扱うこともできます。