Prakhar_Pathak
Prakhar_Pathak

Reputation: 99

Can't perform CRUD operation using custom content provider

I had just learned about Content Provider throught android's official documentation and using that documentation and youtube, I tried to create my own custom content provider using a simple UserDetails database, which I created using RoomDB. When I perform insert operation into the database without using content provider class, it is working perfectly fine but when I click on the button to insert a row using content provider class, my app gets crashed with the error message as :

2024-03-29 06:24:37.837  7528-7528  AndroidRuntime          com.example.contentproviderdemo      E  FATAL EXCEPTION: main
                                                                                                    Process: com.example.contentproviderdemo, PID: 7528
                                                                                                    java.lang.SecurityException: No persistable permission grants found for UID 10206 and Uri content://com.example.customcontentproviderdemo.AcronymProvider/...
                                                                                                        at android.os.Parcel.createExceptionOrNull(Parcel.java:3057)
                                                                                                        at android.os.Parcel.createException(Parcel.java:3041)
                                                                                                        at android.os.Parcel.readException(Parcel.java:3024)
                                                                                                        at android.os.Parcel.readException(Parcel.java:2966)
                                                                                                        at android.app.IUriGrantsManager$Stub$Proxy.takePersistableUriPermission(IUriGrantsManager.java:249)
                                                                                                        at android.content.ContentResolver.takePersistableUriPermission(ContentResolver.java:2952)
                                                                                                        at com.example.contentproviderdemo.AcronymProvider.insert(AcronymProvider.kt:78)
                                                                                                        at android.content.ContentProvider.insert(ContentProvider.java:1911)
                                                                                                        at android.content.ContentProvider$Transport.insert(ContentProvider.java:446)
                                                                                                        at android.content.ContentResolver.insert(ContentResolver.java:2209)
                                                                                                        at android.content.ContentResolver.insert(ContentResolver.java:2171)
                                                                                                        at com.example.contentproviderdemo.MainActivity$onCreate$1$1$1$1$3.invoke(MainActivity.kt:117)
                                                                                                        at com.example.contentproviderdemo.MainActivity$onCreate$1$1$1$1$3.invoke(MainActivity.kt:110)
                                                                                                        at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:895)
                                                                                                        at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:889)
                                                                                                        at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)
                                                                                                        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                                                                                                        at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:177)
                                                                                                        at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
                                                                                                        at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:665)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:544)
                                                                                                        at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:566)
                                                                                                        at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:855)
                                                                                                        at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:703)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:317)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
                                                                                                        at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
                                                                                                        at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
                                                                                                        at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
                                                                                                        at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
                                                                                                        at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)
                                                                                                        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
                                                                                                        

I tried implementing runtime permission handling in my code but it is not working(You may see it in my MainActivity.kt code file, the implementation of "take permission" button). I also tried implementing "takePersistableUriPermission" in my content provider class, but it is also not working. I humbly request to help me to solve this problem. My app's simple goal is to perform CRUD operations on UserDetails database via both ways normal(using viewModel) as well as contentProvider class. My code files are :

  1. Android Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />

    <application
        android:name=".MyApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ContentProviderDemo"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ContentProviderDemo">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider
            android:authorities="com.example.contentproviderdemo.AcronymProvider"
            android:name=".AcronymProvider"
            android:exported="true"
            android:grantUriPermissions="true"/>

    </application>

</manifest>
  1. UserDetails.kt
package com.example.contentproviderdemo.data

import android.content.ContentValues
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class UserDetails(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "_ID") val id: Long = 0L,
    val name : String,
    val age: Int,
    val gender: String,
    val address: String
)

{
    companion object{
        fun fromContentValues(values: ContentValues): UserDetails{
            return UserDetails(
                name = values.getAsString("name") ?: "",
                age = values.getAsInteger("age") ?: 0,
                gender = values.getAsString("gender") ?: "",
                address = values.getAsString("address") ?: ""
            )
        }
    }

}
  1. UserDetailsDao.kt
package com.example.contentproviderdemo.data

import android.database.Cursor
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDetailsDao {

    @Insert
    suspend fun addAUser(userDetails: UserDetails): Long

    @Update
    suspend fun updateAUser(userDetails: UserDetails): Int

    @Delete
    suspend fun deleteAUser(userDetails: UserDetails): Int

    @Query("Select * from UserDetails")
    fun getAllUsers() : Flow<List<UserDetails>>

    @Query("Select * from UserDetails")
    fun getAllUsersCursor() : Cursor?

}
  1. UserDetailsDatabase.kt
package com.example.contentproviderdemo.data

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(
    entities = [UserDetails::class],
    version = 1,
    exportSchema = false
)
abstract class UserDetailsDatabase: RoomDatabase() {

    abstract fun userDetailsDao() : UserDetailsDao

}
  1. Graph.kt
package com.example.contentproviderdemo

import android.content.Context
import androidx.room.Room
import com.example.contentproviderdemo.data.UserDetailsDatabase

object Graph {

    lateinit var database: UserDetailsDatabase

    val userDetailsRepository by lazy {
        UserDetailsRepository(database.userDetailsDao())
    }

    fun init(context: Context){
        database = Room.databaseBuilder(context,UserDetailsDatabase::class.java,"UserDetailsDatabase.db").build()
    }

}
  1. MyApp.kt
package com.example.contentproviderdemo

import android.app.Application

class MyApp: Application() {
    override fun onCreate() {
        super.onCreate()
        Graph.init(this)
    }
}
  1. MainActivity.kt
package com.example.contentproviderdemo

import android.Manifest
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.contentproviderdemo.data.UserDetails
import com.example.contentproviderdemo.ui.theme.ContentProviderDemoTheme
import kotlinx.coroutines.launch
import java.io.File

class MainActivity : ComponentActivity() {

    private val viewModel by viewModels<UserDetailsViewModel>()
    private val permissionsToRequest = arrayOf(
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    )


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ContentProviderDemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val permissionsViewModel by viewModels<PermissionsViewModel>()

                    val dialogQueue = permissionsViewModel.visiblePermissionDialogQueue


                    val multiplePermissionResultLauncher = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.RequestMultiplePermissions(),
                        onResult = {perms->
                            permissionsToRequest.forEach{
                                    permission->
                                permissionsViewModel.onPermissionResult(
                                    permission = permission,
                                    isGranted = perms[permission] == true
                                )
                            }

                        }
                    )

                    val scope= rememberCoroutineScope()
                    val userDetails = UserDetails(
                        name = "Rajat",
                        age = 12,
                        gender = "Male",
                        address = "New colony, Vadodara"
                    )

                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(16.dp),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {

                        Button(onClick = {
                            multiplePermissionResultLauncher.launch(
                                arrayOf(
                                    Manifest.permission.READ_EXTERNAL_STORAGE,
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                                )
                            )
                        }) {
                            Text(text = "Take Permissions")
                        }

                        Button(onClick = {
                            scope.launch {
                                try {
                                    viewModel.insertAUser(userDetails)
                                }catch (e:Exception){
                                    Log.d("MainActivityResponse","Error occured : ${e.message}")
                                }
                            }
                        }) {
                            Text(text = "Click")
                        }
                        Button(onClick = {
                            val cv = ContentValues().apply {
                                put(AcronymProvider._NAME, "Ramesh")
                                put(AcronymProvider._ADDRESS, "New vidarbh, Bengal")
                                put(AcronymProvider._AGE, 45)
                                put(AcronymProvider._GENDER, "Male")
                            }
                            contentResolver.insert(AcronymProvider.CONTENT_URI, cv)
                        }) {
                            Text(text = "Insert using content provider")
                        }

                    }

                    dialogQueue
                        .reversed()
                        .forEach { permission->
                            PermissionDialog(
                                permissionTextProvider = when(permission){

                                    Manifest.permission.READ_EXTERNAL_STORAGE -> {
                                        ReadExternalStoragePermissionTextProvider()
                                    }
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE -> {
                                        WriteExternalStoragePermissionTextProvider()
                                    }
                                    else->return@forEach
                                },
                                isPermanentlyDeclined = !shouldShowRequestPermissionRationale(
                                    permission
                                ),
                                onDismiss = permissionsViewModel::dismissDialog,
                                onOkClick = {
                                    permissionsViewModel.dismissDialog()
                                    multiplePermissionResultLauncher.launch(
                                        arrayOf(permission)
                                    )

                                },
                                onGoToAppSettingsClick = ::OpenAppSettings
                            )

                        }

                }
            }
        }
    }
}

fun Activity.OpenAppSettings(){
    Intent(
        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
        Uri.fromParts("package",packageName,null)
    ).also {
        startActivity(it)
    }
}


  1. Custom Content Provider Class
package com.example.contentproviderdemo

import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.Intent
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import com.example.contentproviderdemo.data.UserDetails
import com.example.contentproviderdemo.data.UserDetailsDatabase
import kotlinx.coroutines.runBlocking

class AcronymProvider : ContentProvider() {

    companion object{
        val PROVIDER_NAME = "com.example.contentproviderdemo.AcronymProvider"
        val URL = "content://$PROVIDER_NAME/UserDetails"
        val CONTENT_URI = Uri.parse(URL)
        private const val USER_DETAILS_CODE = 1
        private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(PROVIDER_NAME, "UserDetails", USER_DETAILS_CODE)
        }


        //Allowing user to access column name of the table directly
        val _ID = "_ID"
        val _NAME = "name"
        val _AGE = "age"
        val _GENDER = "gender"
        val _ADDRESS = "address"
    }


    private lateinit var db: UserDetailsDatabase


    override fun onCreate(): Boolean {
        context?.let {
            Graph.init(it)
            db = Graph.database
        }
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val cursor : Cursor?
        when(uriMatcher.match(uri)){

            USER_DETAILS_CODE-> {
                cursor = db.userDetailsDao().getAllUsersCursor()
                if (cursor != null) {
                    cursor.setNotificationUri(context!!.contentResolver, uri)
                }
            }
            else->{
                throw IllegalArgumentException("Unknown Uri: $uri")
            }
        }
        return cursor
    }

    override fun getType(uri: Uri): String? {
        return when(uriMatcher.match(uri)){
            USER_DETAILS_CODE-> "vnd.android.cursor.dir/vnd.$PROVIDER_NAME.UserDetails"
            else -> throw IllegalArgumentException("Unknown Uri $uri")
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {

        context?.contentResolver?.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )

        var insertedRowId: Long = 0
        when (uriMatcher.match(uri)) {
            USER_DETAILS_CODE -> {
                val userDetails = values?.let { UserDetails.fromContentValues(it) }
                runBlocking {
                    userDetails?.let {
                        insertedRowId = db.userDetailsDao().addAUser(it)
                    }
                }
            }

            else -> throw IllegalArgumentException("Unknown Uri: $uri")
        }

        if (insertedRowId != 0L) {
            context?.contentResolver?.notifyChange(uri, null)
        }

        return ContentUris.withAppendedId(uri, insertedRowId)
    }
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {

        context?.contentResolver?.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )

        var rowsDeleted = 0
        when (uriMatcher.match(uri)){
            USER_DETAILS_CODE->{
                runBlocking {
                    rowsDeleted = db.userDetailsDao().deleteAUser(
                        UserDetails.fromContentValues(ContentValues().apply {
                            put("_ID", uri.lastPathSegment)
                        })
                    )
                }

            }
            else -> throw IllegalArgumentException("Unknown Uri $uri")
        }
        if (rowsDeleted != 0){
            context!!.contentResolver.notifyChange(uri,null)
        }
        return rowsDeleted
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {

        context?.contentResolver?.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )

        var rowsUpdated = 0
        when(uriMatcher.match(uri)){
            USER_DETAILS_CODE->{
                runBlocking {
                    rowsUpdated = db.userDetailsDao().updateAUser(
                        UserDetails.fromContentValues(values!!)
                    )
                }

            }
            else -> throw IllegalArgumentException("Unknown Uri $uri")
        }
        if (rowsUpdated != 0){
            context!!.contentResolver.notifyChange(uri,null)
        }
        return rowsUpdated
    }
}

I had to remove some of the parts because my question's body was exceeding the limit, it includes viewModel(generally used to perform CRUD operations normally), Repository, parts of runtime permission handling like rationale layout and Permission's view Model. I don't think they are necessary to solve this problem. Feel free to ask, if anybody needs to see that code file too. I will share them too.

Upvotes: 0

Views: 38

Answers (0)

Related Questions