The article directories

  • introduce
  • The basic usage of ContentResolver
    • Knowledge reserves
    • Reading system contacts
  • Create your own ContentProvider
    • Create the ContentProvider step
    • Implement cross-program data sharing

introduce

ContentProvider is used to share data between different applications. It provides a complete mechanism that allows one program to access data in another program while ensuring the security of the accessed data. Currently, using ContentProviders is the standard way for Android to share data across applications

Unlike the two global read-write modes in the file store and SharedPreferences store, ContentProvider can choose which part of the data to share, thus ensuring that the private data in our application is not at risk of disclosure

The usage of ContentProvider generally has two kinds: one is to use the existing ContentProvider to read and operate the corresponding program data; Another is to create your own ContentProvider to provide external access to the program’s data

The basic usage of ContentResolver

Knowledge reserves

For every application that wants to access the shared data in a ContentProvider, it uses the ContentResolver class, an instance of which can be obtained using the getContentResolver() method. ContentResolver provides a series of methods for adding, deleting, modifying, and querying data: insert() adds data; Update () updates data; Delete () deletes data; Query () queries data

Receives a Uri parameter, called the content Uri. The content URI establishes a unique identifier for the data in the ContentProvider, which consists of the Authority and path. Authority is used to distinguish between different applications and is usually named using package names to avoid conflicts. For example, if the package name of an application is com.example.app, the authority of the application can be named com.example.app.provider. Path is used to distinguish between different tables in the same application and is usually appended to authority. Select * from table1; select * from table2; select * from table1; select * from table2; select * from table1; Content uris becomes com. Example. App. The provider/table1 and com. Example. The app. The provider/table2. Finally, a protocol declaration is required at the head of the string. Therefore, the most standard format for a content URI is:

content://com.example.app.provider/table1
content://com.example.app.provider/table2
Copy the code

It then needs to be parsed into a Uri object to be passed in as a parameter:

val uri = Uri.parse("content://com.example.app.provider/table1")
Copy the code

Now you can use this Uri object to query the data in table1:

val cursor = contentResolver.query(
	uri,
	projection,
	selection,
	selectionArgs,
	sortOrder
)
Copy the code

It is a projection that specifies the query column name. Selection specifies where constraints. SelectionArgs provides specific values for placeholders. SortOrder Specifies how to sort the query results

After the query is complete, the Cursor object is still Cursor, and the reading method is as follows:

while(cursor.moveToNext()){
	val column1 = cursor.getString(cursor.getColumnIndex("column1"))
	val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.cloase()
Copy the code

Add a piece of data like this:

val values = contentValuesOf("column1" to "text"."column2" to 1)
contentResolver.insert(uri,values)
Copy the code

If you want to update this column and clear column1, use the ContentResolver update() method:

val values = contentValuesOf("column1" to "")
contentResolver.update(uri,values,"column1 = ? and column2 = ?",arrayOf("text"."1"))
Copy the code

Note that the code above uses the selection and selectionArgs parameters to constrain all rows from being affected

Delete this data:

contentResolver.delete(uri,"column2 = ?",arrayOf("1))
Copy the code

Next, we use the knowledge we have learned to read the contact information in the system address book

Reading system contacts

layout

<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/contactsView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
Copy the code

code

class TestActivity : AppCompatActivity() {
    private val contactsList = ArrayList<String>()
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
        contactsView.adapter = adapter
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.READ_CONTACTS ) ! = PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)}else {
            readContacts()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                readContacts()
            } else {
                Toast.makeText(this."You denied the permission", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun readContacts(a) {
        contentResolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null.null.null.null)? .apply {while (moveToNext()) {
                // Get the contact name
                val displayName =
                    getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                // Get the contact number
                val number =
                    getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                contactsList.add("$displayName\n$number")
            }
            adapter.notifyDataSetChanged()
            close()
        }
    }
}
Copy the code

Permissions in AndroidManifest

<uses-permission android:name="android.permission.READ_CONTACTS"/>
Copy the code

Operation results (add some contact data in simulator in advance)

Create your own ContentProvider

Create the ContentProvider step

To share data across programs, create a new class that inherits the ContentProvider approach

class MyProvider : ContentProvider() {
    /** * This is used when initializing the ContentProvider. This is usually used when creating or upgrading a database
    override fun onCreate(a): Boolean {
        return false
    }

    /** * Query data from ContentProvider */
    override fun query(
        uri: Uri,
        projection: Array<out String>? , selection:String? , selectionArgs:Array<out String>? , sortOrder:String?).: Cursor? {
        TODO("Not yet implemented")}/** * Adds a data to the ContentProvider * URI: identifies the table to be added to * VALUES: Data to be added * returns a URI to represent the new record */
    override fun insert(uri: Uri, values: ContentValues?).: Uri? {
        TODO("Not yet implemented")}/** * Update existing data in ContentProvider * URI: which table to update * VALUES: Update data stored in this parameter * selection\selectionArgs to constrain which rows to update * Return value: affected rows */
    override fun update(
        uri: Uri,
        values: ContentValues? , selection:String? , selectionArgs:Array<out String>?: Int {
        TODO("Not yet implemented")}/** * Remove data from ContentProvider * Return value: deleted line */
    override fun delete(uri: Uri, selection: String? , selectionArgs:Array<out String>?: Int {
        TODO("Not yet implemented")}/** * Returns the MIME type */ based on the content URI passed in
    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")}}Copy the code

In retrospect, the content of a standard URI are written like this: the content: / / com. Example. App. The provider/table1 which means the caller is expected to visit com. Example. The app the table1 of the application of the data in the table

In addition, we can add an ID to the content URI as follows: Content: / / com. Example. App. The provider/table1/1, which means the caller is expected to visit com. Example. The app the table1 id in the table 1 of the application of the data

There are only two formats of content URIs. A URI ending in a path indicates that all data in the table is expected to be accessed, and a URI ending in an ID indicates that data with the corresponding ID in the table is expected to be accessed. We can use wildcards to match both formats of content URIs as follows:

* : matches any character of any length. # : matches a number of any length.

So, a content URI format can match any tables can be written as: the content: / / com. Example. The app. The provider / *

And one can match table1 the content URI format any rows of data in the table can be written as: the content: / / com. Example. The app. The provider/table1 / #

We can then easily match content URIs with the UriMatcher class. UriMatcher provides an addURI() method that takes three arguments, authority, path, and a custom code. This way, when UriMatcher’s match() method is called, a Uri object is passed in and the return value is some custom code that matches the Uri object. Using this code, we can determine which table the caller expects to access. Modify the code in MyProvider as follows:

package com.example.myapplication

import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri

class MyProvider : ContentProvider(a){
    private val table1Dir = 0
    private val table1Item = 1
    private val table2Dir = 2
    private val table2Item = 3

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

    init {
        uriMatcher.addURI("com.example.app.provider"."table1",table1Dir)
        uriMatcher.addURI("com.example.app.provider"."table1/#",table1Item)
        uriMatcher.addURI("com.example.app.provider"."table2",table2Dir)
        uriMatcher.addURI("com.example.app.provider"."table2/#",table2Item)
    }
    /** * This is used when initializing the ContentProvider. This is usually used when creating or upgrading a database
    override fun onCreate(a): Boolean {
        return false
    }

    /** * Query data from ContentProvider */
    override fun query( uri: Uri, projection: Array
       
        ? , selection: String? , selectionArgs: Array
        
         ? , sortOrder: String? )
        
       : Cursor? {
        when(uriMatcher.match(uri)){
            table1Dir -> {
                Select * from table1
            }
            table1Item -> {
                Select * from table1
            }
            table2Dir -> {
                Select * from table2
            }
            table2Item -> {
                Select * from table2}}return null
    }

    /** * Adds a data to the ContentProvider * URI: identifies the table to be added to * VALUES: Data to be added * returns a URI to represent the new record */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Not yet implemented")}/** * Update existing data in ContentProvider * URI: which table to update * VALUES: Update data stored in this parameter * selection\selectionArgs to constrain which rows to update * Return value: affected rows */
    override fun update( uri: Uri, values: ContentValues? , selection: String? , selectionArgs: Array
       
        ? )
       : Int {
        TODO("Not yet implemented")}/** * Remove data from ContentProvider * Return value: deleted line */
    override fun delete(uri: Uri, selection: String? , selectionArgs: Array
       
        ?)
       : Int {
        TODO("Not yet implemented")}/** * Returns the MIME type */ based on the content URI passed in
    override fun getType(uri: Uri): String? {
        TODO("Not yet implemented")}}Copy the code

MyProvider () ¶ MyProvider () ¶ MyProvider () ¶ MyProvider () ¶ MyProvider () ¶ MyProvider () {table1Dir (); We then create an instance of UriMatcher in the static code block and call the addURI() method to pass in the content URI format we expect to match. Note that the path parameters passed in here can use wildcards. When the query() method is called, the Uri object passed in is matched by UriMatcher’s match() method. If a content Uri format in UriMatcher successfully matches the Uri object, the custom code is returned. Then we can figure out what data the caller is expecting to access, right

Insert (), update(), and delete() are all implemented the same way. They all take a Uri. Then, UriMatcher’s match() method is also used to determine which table the caller wants to access, and then perform corresponding operations on the data in the table

There is another method that you may not be familiar with, the getType() method. It is a method that all content providers must provide to get the MIME type of the Uri object. The MIME string corresponding to a content URI consists of three parts. Android has the following formats for these three parts

  • It must start with VND
  • If the content URI ends in a path, it is followedandroid.cursor.dir/;If the content URI ends in id, it is followedandroid.cursor.item/.
  • The last plug invnd.<authority>.<path>

So, for the content: / / com. Example. The app. The provider/table1 the content URI, it corresponds to the MIME type can be written as:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1
Copy the code

For the content: / / com. Example. App. The provider/table1/1 the content URI, it corresponds to the MIME type can be written as:

vnd.android.cursor.item/vnd.com.example.app.provider.table1
Copy the code

Now we can continue to refine MyProvider, this time implementing the logic in the getType() method as follows:

    /** * Returns the MIME type */ based on the content URI passed in
    override fun getType(uri: Uri): String? = when(uriMatcher.match(uri)){
        table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
        table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
        table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
        table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
        else -> null
    }
Copy the code

At this point, a complete content provider is created, and any application can now use the ContentResolver to access the data in our application. So how can we ensure that private data does not leak out? Thanks to the good mechanics of the content provider, this problem has been solved by stealth. Since all CRUD operations must match the corresponding content URI format, it is impossible to add a URI for private data to UriMatcher, so this part of data cannot be accessed by external programs, and there is no security problem

Implement cross-program data sharing

The next steps are to continue development of the SQLite database creation and upgrade project by adding an external access interface to it through the ContentProvider. To open the project, first remove the successful database creation prompt in MyDatabaseHelper with Toast, because we cannot use Toast directly for cross-application access. And then create a Content Provider, right-click the com. Example. Myapplication package – > New – > Other > Content Provider



Here we will content provider named DatabaseProvider, authorities specified for the com. Example. Myapplication. The provider,ExportedProperty indicates whether external programs are allowed access to our content provider,EnabledProperty indicates whether the content provider is enabled. Check both properties and click Finish to complete the creation

The code is as follows:

class DatabaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.example.myapplication.provider"
    private var dbHelper: MyDatabaseHelper? = null

    // By lazy block is a lazy loading technique provided by Kotlin. The code in a block is not executed at first,
    // The uriMatcher variable is executed only when it is first called, and the return value of the last line of code in the block is assigned to uriMatcher
    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", bookDir)
        matcher.addURI(authority, "book/#", bookItem)
        matcher.addURI(authority, "category", categoryDir)
        matcher.addURI(authority, "category/#", categoryItem)
        matcher
    }

    // The delete() method again gets an instance of SQLiteDatabase first
    // Then determine which table the user wants to delete based on the uri parameter passed in
    // Call the delete() method of SQLiteDatabase
    // The number of deleted rows is returned as the return value
    override fun delete(uri: Uri, selection: String? , selectionArgs:Array<String>?= dbHelper? .let {// Delete data
            val db = it.writableDatabase
            val deletedRows = when (uriMatcher.match(uri)) {
                bookDir -> db.delete("Book", selection, selectionArgs)
                bookItem -> {
                    val bookId = uri.pathSegments[1]
                    db.delete("Book"."id = ?", arrayOf(bookId))
                }
                categoryDir -> db.delete(
                    "Category", selection, selectionArgs
                )
                categoryItem -> {
                    val categoryId = uri.pathSegments[1]
                    db.delete("Category"."id = ?", arrayOf(categoryId))
                }
                else -> 0} deletedRows } ? :0

    override fun getType(uri: Uri) = when (uriMatcher.match(uri)) {
        bookDir ->
            "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
        bookItem ->
            "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
        categoryDir ->
            "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
            categoryItem ->
            "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
        else -> null
    }

    // the insert() method starts with an instance of SQLiteDatabase
    // Then determine which table the user wants to add data to based on the Uri parameter passed in
    // Add it by calling the insert() method of SQLiteDatabase
    // Note that the insert() method requires that a URI be returned that represents the new data
    // So we also need to call uri.parse () to parse a content Uri into a Uri object
    // Of course the content URI ends with the ID of the new data
    override fun insert(uri: Uri, values: ContentValues?).= dbHelper? .let {// Add data
        val db = it.readableDatabase
        val uriReturn = when (uriMatcher.match(uri)) {
            bookDir, bookItem -> {
                val newBookId = db.insert("Book".null, values)
                Uri.parse("content://$authority/book/$newBookId")
            }
            categoryDir, categoryItem -> {
                val newCategoryId = db.insert("Category".null, values)
                Uri.parse("content://$authority/category/$newCategoryId")}else -> null
        }
        uriReturn
    }

    // First we call the getContext() method with? The. Operator and the let function determine whether its return value is null
    // If empty, use? The: operator returns false, indicating that the ContentProvider failed to initialize
    // Execute the code in let if it is not null
    // Create an instance of MyDatabaseHelper in the let function and return true to indicate that the ContentProvider initialized successfully
    override fun onCreate(a)= context? .let { dbHelper = MyDatabaseHelper(it,"BookStore".2)
        true
    } ?: false


    // We get an instance of SQLiteDatabase and determine which table the user wants to access based on the Uri parameter passed in
    // Call SQLiteDatabase's query() and return the Cursor object
    // Notice that the getPathSegments() method of the Uri object is called when a single piece of data is accessed
    // It splits the portion after the content URI permission with a "/" and puts the split result into a list of strings
    // The 0th position in the list is the path
    // The id is stored in the first position
    // Select args and selectionArgs; // Select args and selection args
    override fun query(
        uri: Uri, projection: Array<String>? , selection:String? , selectionArgs:Array<String>? , sortOrder:String?).= dbHelper? .let {// Query data
        val db = it.readableDatabase
        val cursor = when (uriMatcher.match(uri)) {
            bookDir -> {
                db.query("Book", projection, selection, selectionArgs, null.null, sortOrder)
            }
            bookItem -> {
                val bookId = uri.pathSegments[1]
                db.query("Book", projection, "id = ?", arrayOf(bookId), null.null, sortOrder)
            }
            categoryDir -> {
                db.query("Category", projection, selection, selectionArgs, null.null, sortOrder)
            }
            categoryItem -> {
                val categoryId = uri.pathSegments[1]
                db.query(
                    "Category",
                    projection,
                    "id = ?",
                    arrayOf(categoryId),
                    null.null,
                    sortOrder
                )
            }
            else -> null
        }
        cursor
    }

    The update() method also starts by getting an instance of SQLiteDatabase
    // Then determine which table the user wants to update based on the Uri parameter passed in
    // Call the update() method of SQLiteDatabase
    // The number of affected rows is returned as a return value
    override fun update(
        uri: Uri, values: ContentValues? , selection:String? , selectionArgs:Array<String>?= dbHelper? .let {// Update data
        val db = it.writableDatabase
        val updatedRows = when (uriMatcher.match(uri)) {
            bookDir -> db.update(
                "Book", values, selection, selectionArgs
            )
            bookItem -> {
                val bookId = uri.pathSegments[1]
                db.update("Book", values, "id = ?", arrayOf(bookId))
            }
            categoryDir -> db.update(
                "Category", values, selection, selectionArgs
            )
            categoryItem -> {
                val categoryId = uri.pathSegments[1]
                db.update(
                    "Category", values, "id = ?", arrayOf(categoryId)
                )
            }
            else -> 0} updatedRows } ? :0
}
Copy the code

The ContentProvider must be registered in the Androidmanifest.xml file to use it. Since we created the ContentProvider using shortcuts from Android Studio, the registration step is already done automatically. Open the AndroidManifest. XML

<? xml version="1.0" encoding="utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">... <application android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication">
        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.example.myapplication.provider"
            android:enabled="true"
            android:exported="true"></provider>
		......
    </application>

</manifest>
Copy the code

A new tag < Provider > appears in the < Application > TAB, which we use to register DatabaseProvider. The Android: Name attribute specifies the DatabaseProvider class name, and the Android: Authorities attribute specifies the DatabaseProvider authority. The Enabled and exported properties are automatically generated based on the status we just checked, which allows the DatabaseProvider to be accessed by other applications

Now that the project has the ability to share data across applications, let’s give it a try. First, we need to remove the program from the simulator to prevent the legacy data from the previous chapter from interfering with us. Then run the project and reinstall the program on the emulator

Close the project and create a new project, ProviderTest. We will use this program to access the data from the previous project, write the layout file, and modify the code in activity_main.xml:


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/addData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add To Book" />
    <Button
        android:id="@+id/queryData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query From Book" />
    <Button
        android:id="@+id/updateData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update Book" />
    <Button
        android:id="@+id/deleteData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete From Book" />
</LinearLayout>
Copy the code

The layout file is simple, with four buttons for adding, querying, updating, and deleting data. Then modify the code in MainActivity as follows:

class MainActivity : AppCompatActivity() {
    var bookId: String? = null
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // When adding data, the uri.parse () method is first called to parse a content Uri into a Uri object
        // Then store the data to be added into the ContentValues object
        // Then call the insert() method of the ContentResolver to add
        // Note that the insert() method returns a Uri object containing the ID of the new data
        // We take this ID out with the getPathSegments() method, which we'll use later.
        addData.setOnClickListener {
            // Add data
            val uri = Uri.parse("content://com.example.myapplication.provider/book")
            val values = contentValuesOf(
                "name" to "A Clash of Kings"."author" to "George Martin"."pages" to 1040."price" to 22.85
            )
            valnewUri = contentResolver.insert(uri, values) bookId = newUri? .pathSegments? .get(1)}// When querying data, the uri.parse () method is also called to parse a content Uri into a Uri object
        // Then call the ContentResolver's query() method to query the data
        // Query results are stored in Cursor objects
        // Print the query result at the Cursor.
        queryData.setOnClickListener {
            // Query data
            val uri = Uri.parse("content://com.example.myapplication.provider/book")
            contentResolver.query(uri, null.null.null.null)? .apply {while (moveToNext()) {
                    val name = getString(getColumnIndex("name"))
                    val author = getString(getColumnIndex("author"))
                    val pages = getInt(getColumnIndex("pages"))
                    val price = getDouble(getColumnIndex("price"))
                    Log.d("MainActivity"."book name is $name")
                    Log.d("MainActivity"."book author is $author")
                    Log.d("MainActivity"."book pages is $pages")
                    Log.d("MainActivity"."book price is $price")
                }
                close()
            }
        }
        // When updating data, the content URI is resolved into a URI object, and the data to be updated is stored in a ContentValues object
        // Call the ContentResolver update() method to perform the update
        // Here we add the ID to the end of the content Uri when calling uri.parse (), so as not to affect other rows in the Book table
        // This is the id that is returned when data is added. This means that we only want to update the data we just added, and no other rows in the Book table will be affected
        updateData.setOnClickListener {
            // Update databookId? .let {val uri =
                    Uri.parse("content://com.example.myapplication.provider/book/$it")
                val values = contentValuesOf(
                    "name" to "A Storm of Swords"."pages" to 1216."price" to 24.05
                )
                contentResolver.update(uri, values, null.null)}}// The same method is used to resolve a content URI ending in id when deleting data
        // Then call the ContentResolver's delete() method to perform the delete operation
        // Since we specify an ID in the content URI, only the row with the corresponding ID will be deleted. Other data in the Book table will not be affected

        deleteData.setOnClickListener {
            // Delete databookId? .let {val uri =
                    Uri.parse("content://com.example.myapplication.provider/book/$it")
                contentResolver.delete(uri, null.null)}}}}Copy the code