Using SharedPreferences via ContentProvider

公開日: 2020年08月14日最終更新日: 2022年01月28日

Consider how to share small amounts of key-value data between apps.

ContentProvider is a system for managing access to your data. You can choose how you want to store your data.
There are several ways to store data on Android, including File, SQLite, Cloud and SharedPreference.

Here, we will consider the case of sharing simple and small amount of data (e.g. one key-value data).

For a single key-value data, you'd want to store it in SharedPreference, but SharedPreference can't be shared between apps. (Until API level 17, SharedPreference had MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE, but they are not available now. Also, it does not support access from multiple processes.

The first way I can think of to share the data is to create a database, store the key-value data and access it via ContentProvider.
You'll probably be fine this way. However, I think it's a bit of a stretch to provide CRUD functionality by creating a table just to share a single key-value data.

So this time, I tried to see if it would be possible to share data easily by storing key-value data in SharedPreference and providing access to the data using ContentProvider.call().
The idea is that by using ContentProvider.call, we can call the get / put method provided by the provider to access the data.

ContentProvider.call()

ContentProvider.call(), you can call the method defined by ContentProvider.

Implement ContentProvider.call() as follows: Using the method name and arguments received from the ContentProvider, you call the corresponding method and return the result to the Bundle.
This allows you to call methods across processes without having to define aidingl by yourself for inter-process communication.

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
}

Access SharedPreferences via the ContentProvider.call().

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()
    }
}

We don't use it, but you need to override insert(), query(), update(), delete(), and getType() in the ContentProvider you create.

And don't forget to add the <provider> tag to the <application> tag in the AndroidManifest.xml of the app that provides ContentProvider.

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

ContentResolver.call()

The client side of ContentProvider is 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")
}

When using ContentProvider, you must abide by the conventions of the URIs and method names.
These decisions are gathered in a contract class and shared between applications.
The following is a list of contract classes that summarize the rules such as URIs and method names for use in this article.

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 Settings

ContentProvider allows you to set your own permissions as needed. For more information, see Implementing Permissions.

Define the permission in AndroidManifest.xml and use it in the provider.
Add the following permissions in the <manifest> tag.

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

For more information about permissions, please see the following:
https://developer.android.com/guide/topics/manifest/permission-element

Set the above permission defined in android:permission in the <provider> tag.

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

The following permissions are available for the provider.

  • android:grantUriPermissions: Temporary permission flag.
  • android:permission: Single provider-wide read/write permission.
  • android:readPermission: Provider-wide read permission.
  • android:writePermission: Provider-wide write permission.

SharedPreferences

For more information about SharedPreferences, see Save Key-Value Data has an explanation.

I think that you may use getSharedPreferences, getPreferences, getDefaultSharedPreferences or getSharedPreferences because I do not assume a complicated case this time.

It is used as follows.

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()
    }
}

Source code of the sample app

The source code for the sample app that I created through trial and error while researching this time is in the following repository.

In the sample, MyPreferences is simply copied, but in fact, the data provider should be able to use it from the user by making it a library.
If you create a Manager class, you can handle it completely with method calls.