Dear learners, you must have listen about these two terms when you started your development journey in Android. Today we will learn about the two confusing terms – Content Provider & Content Resolver. In this article we learn about following topics:
- Difference B/w URI & URL & Uri
- What is SQlite Database
- Difference B/w Content Provider & Content Resolver
- How to create Content Provider
The main role of content provider in your app is to provide a central repository of data which is accessible by other apps as well as solves its own requirement. For example consider our Contacts app which uses Content Provider to provides contacts to other apps along with it uses for its own functioning. A contact app stores its data in SQlite database which maintain the records of all the contacts in your device. Also when other apps need to fetch contacts of the device then they use the content provider of the Contacts app and fetches all the contacts. Similarly other examples of app which use Content Providers are Calls App, SMS app, Notes app etc.
Before start learning about Content Resolver and Content Provider, we need to understand some other important terms for more clarification like URI, URL & Uri etc.
Understanding URI, URL, and Uri
URI (Uniform Resource Identifier), URL (Uniform Resource Locator), and Uri
(Android-specific class) are all related to identifying resources, but they serve different purposes and are used in different contexts. Let’s break them down.
1. URI (Uniform Resource Identifier)
A URI is a general term that identifies a resource either by location, name, or both. It provides a way to reference a resource in a uniform way but doesn’t necessarily provide access to the resource.
A URI has two types:
- URN (Uniform Resource Name): Identifies a resource by name, independent of its location.
- Example:
urn:isbn:9780134685991
(An identifier for a book by ISBN number).
- Example:
- URL (Uniform Resource Locator): A subset of URI that specifies both where a resource is located and how to access it.
Examples:
http://example.com/resource
(This is a URL, which is also a URI.)ftp://example.com/file
(An FTP location, also a URI.)urn:isbn:9780134685991
(A name-based identifier, not a URL.)
2. URL (Uniform Resource Locator)
Definition:
A URL is a specific type of URI that not only identifies a resource but also provides the means to access it, usually by specifying the protocol (e.g., HTTP, FTP).
- Always contains a scheme (protocol) and location information.
- Indicates how to access the resource (e.g., via HTTP, HTTPS, FTP).
A URL typically includes:
- Scheme: Specifies the protocol (e.g.,
http
,https
,ftp
). - Host: The domain or IP address of the server (e.g.,
example.com
). - Path: The specific resource path on the server (e.g.,
/index.html
). - Query Parameters (optional): Extra data sent to the server (e.g.,
?id=123
).
Examples:
https://www.example.com/index.html
(A webpage accessed via HTTPS.)ftp://files.example.com/download.zip
(A file accessed via FTP.)
3. Uri (Android-specific Class)
Definition:
In Android, Uri
is a Kotlin/Java class that represents a URI reference. It is used extensively to handle resource identifiers in the Android system, such as content URIs (content://
), file paths (file://
), or web URLs.
- Designed specifically for Android.
- Makes it easier to manipulate and parse URIs in Android apps.
Common Use Cases:
- Content URIs: Access data from content providers.
- Example:
content://com.example.provider/users
- Example:
- File URIs: Access files on the device.
- Example:
file:///storage/emulated/0/DCIM/image.jpg
- Example:
- Network URLs: Access web resources.
- Example:
https://example.com/api/dat
- Example:
Use Cases in Android
- Access Content Providers:
val uri: Uri = Uri.parse("content://com.example.provider/users")
val cursor = contentResolver.query(uri, null, null, null, null)
2. Access Files:
val uri: Uri = Uri.parse("file:///storage/emulated/0/Download/example.txt")
3. Parse URLs:
val uri: Uri = Uri.parse("https://www.example.com")
println(uri.scheme) // "https"
println(uri.host) // "www.example.com"
So, this is all about the URI, URL and Uri. Now we will learn about how android stores data behind the scenes.
How android Stores data- SQLite Database
Android uses SQlite data which is a Relational Database and uses SQL for data manipulations. Android has these all types of storage where it stores all the types based on type of data, size of data, accessibility of data etc. But when comes to storing structured data android uses SQLite database which is a offline first and very fast and easy to manage database and a lite version of MySQL database.
Storage Type | Use Case | Access | Example |
---|---|---|---|
Internal Storage | Private app data | Only app | Settings, temp files |
External Storage | Public/shared files | App + others | Photos, videos |
SharedPreferences | Lightweight key-value pairs | Only app | User preferences |
SQLite/Room | Structured relational data | Only app | To-do list, contacts |
Cache | Temporary data | Only app | Image cache |
MediaStore | Shared media files | App + others | Public images, videos |
Cloud Storage | Data sync/backup | App + cloud | Firebase Storage, Google Drive |
Relational Database
A relational database is a type of database that organizes data into rows and columns, which collectively form a table where the data points are related to each other. Data is typically structured across multiple tables, which can be joined together via a primary key or a foreign key.
For example, imagine your company maintains a database table with customer information, which contains company data at the account level. There may also be a different table, which describes all the individual transactions that align to that account. Together, these tables can provide information about the different industries that purchase a specific software product.
Relational Database vs. Relational Database management system
While a relational database organizes data based off a relational data model, a relational database management system (RDBMS) is a more specific reference to the underlying database software that enables users to maintain it. These programs allow users to create, update, insert, or delete data in the system. Examples of popular RDBMS systems include MySQL, PostgreSQL
What is SQL?
Structured Query Language (SQL) is the standard programming language for interacting with relational database management systems, allowing database administrator to add, update, or delete rows of data easily. Originally known as SEQUEL, it was simplified to SQL due to a trademark issue. SQL queries also allows users to retrieve data from databases using only a few lines of code. Given this relationship, it’s easy to see why relational databases are also referred to as “SQL databases” at times.
Why Use SQL?
- Data Storage: Store and organize data in tables.
- Data Retrieval: Query and fetch specific data.
- Data Manipulation: Add, update, or delete data.
- Data Analysis: Aggregate and filter data for reporting.
- Database Management: Create and modify database structures.
Basic SQL Operations
Here’s how you can use SQL:
1. Create a Database
sqlCopy codeCREATE DATABASE School;
2. Create a Table
sqlCopy codeCREATE TABLE Students (
ID INT PRIMARY KEY,
Name VARCHAR(50),
Age INT,
Grade VARCHAR(5)
);
3. Insert Data into a Table
sqlCopy codeINSERT INTO Students (ID, Name, Age, Grade)
VALUES (1, 'Alisha', 20, 'A');
4. Retrieve Data (SELECT Query)
- Fetch all records:sqlCopy code
SELECT * FROM Students;
- Fetch specific columns:sqlCopy code
SELECT Name, Grade FROM Students;
- Add a condition:sqlCopy code
SELECT * FROM Students WHERE Grade = 'A';
5. Update Data
sqlCopy codeUPDATE Students
SET Grade = 'B'
WHERE ID = 1;
6. Delete Data
sqlCopy codeDELETE FROM Students WHERE ID = 1;
7. Aggregate Functions
- Count records:sqlCopy code
SELECT COUNT(*) FROM Students;
- Find the average age:sqlCopy code
SELECT AVG(Age) FROM Students;
8. Join Tables
sqlCopy codeSELECT Students.Name, Courses.CourseName
FROM Students
JOIN Courses
ON Students.ID = Courses.StudentID;
So, till now have a basic idea about what are these all terms and their use cases, now we learn how to android use them for its own data management and what relation does content resolver and content provider have with all these.
Difference b/w Content Provider & Content Resolver
What is Content Resolver
The Content Resolver is the single, global instance in your application that provides access to your (and other applications’) content providers. The Content Resolver behaves exactly as its name implies: it accepts requests from clients, and resolves these requests by directing them to the content provider with a distinct authority. To do this, the Content Resolver stores a mapping from authorities to Content Providers. This design is important, as it allows a simple and secure means of accessing other applications’ Content Providers.
The Content Resolver includes the CRUD (create, read, update, delete) methods corresponding to the abstract methods (insert, query, update, delete) in the Content Provider class. The Content Resolver does not know the implementation of the Content Providers it is interacting with (nor does it need to know); each method is passed an URI that specifies the Content Provider to interact with.
What is Content Provider
Whereas the Content Resolver provides an abstraction from the application’s Content Providers, Content Providers provide an abstraction from the underlying data source (i.e. a SQLite database). They provide mechanisms for defining data security (i.e. by enforcing read/write permissions) and offer a standard interface that connects data in one process with code running in another process.
Content Providers provide an interface for publishing and consuming data, based around a simple URI addressing model using the content://
schema. They enable you to decouple your application layers from the underlying data layers, making your application data-source agnostic by abstracting the underlying data source.
Understand these two with a simple analogy
Suppose you go to groceries stores to buy some items for you home need. In the shop you interact with Counter person and told him your requirements of items, then he tells the helper to brought your items from the store, then he brought items one by one as you have asked and give it to the Counter person and then the counter person will give that to you. So here the Counter Person will be work as Content Resolver and the helper will work as Content Provider as Content Resolver(Counter person) resolves your query by asking the required item(Data) from the Content Provider(Helper) in the store(App).
How to create our custom Content Provider
Now lets begin our journey to create our own content provider which will provide simple user data which we will insert or other apps inserts in the app. As you already know that we need to make a SQLite database in our app which will stores our apps data that in turn we will provide to other apps.
In this app will make a simple database which contains only one table which stores id and name field. So to make any class a Provider class we need to extends it with ContentProvider() and you need to override some function which helps you to manipulate data in the content provider. When you make a call to content resolvers then he will call these function as required.
class class UserProvider:ContentProvider() {
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}
override fun query(
p0: Uri,
p1: Array<out String>?,
p2: String?,
p3: Array<out String>?,
p4: String?
): Cursor? {
TODO("Not yet implemented")
}
override fun getType(p0: Uri): String? {
TODO("Not yet implemented")
}
override fun insert(p0: Uri, p1: ContentValues?): Uri? {
TODO("Not yet implemented")
}
override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
TODO("Not yet implemented")
}
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
TODO("Not yet implemented")
}
}
Now we need to define some variables and constants which we need in our Content Provider class. lets define these in companion object.
companion object {
// defining authority so that other application can access it
const val PROVIDER_NAME = "com.example.learngit"
// defining content URI
const val URL = "content://$PROVIDER_NAME/users"
// parsing the content URI
val CONTENT_URI = Uri.parse(URL)
const val id = "id"
const val name = "name"
const val uriCode = 1
var uriMatcher: UriMatcher? = null
private val values: HashMap<String, String>? = null
// declaring name of the database
const val DATABASE_NAME = "UserDB"
// declaring table name of the database
const val TABLE_NAME = "Users"
// declaring version of the database
const val DATABASE_VERSION = 1
// sql query to create the table
const val CREATE_DB_TABLE =
(" CREATE TABLE " + TABLE_NAME
+ " (id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ " name TEXT NOT NULL);")
init {
// to match the content URI
// every time user access table under content provider
uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
// to access whole table
uriMatcher!!.addURI(
PROVIDER_NAME,
"users",
uriCode
)
// to access a particular row
// of the table
uriMatcher!!.addURI(
PROVIDER_NAME,
"users/*",
uriCode
)
}
}
Now lets make a custom class which will make our database. This class will extends SLiteOpenHelper helper class which provides methods to create or update databases. here you need to override two function namely onCreate and onUpgrade.
// creating object of database
// to perform query
private var db: SQLiteDatabase? = null
// creating a database
private class DatabaseHelper // defining a constructor
(context: Context?) : SQLiteOpenHelper(
context,
DATABASE_NAME,
null,
DATABASE_VERSION
) {
// creating a table in the database
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_DB_TABLE)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
// sql query to drop a table
// having similar name
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
onCreate(db)
}
}
After this we will write code for each override function and understand with more clarity. Our first override function is onCreate in which we only initialize our database.
// creating the database
override fun onCreate(): Boolean {
val context = context
val dbHelper = DatabaseHelper(context)
db = dbHelper.writableDatabase
return if (db != null) {
true
} else false
}
now comes to our 2nd and main query function which will take some arguments and return a Cursor object which contains you queried data. our query function takes following arguments
// Queries the UserDictionary and returns results
cursor = contentResolver.query(
CONTENT_URI, // The content URI of the words table
projection, // The columns to return for each row
selectionClause, // Selection criteria
selectionArgs.toTypedArray(), // Selection criteria
sortOrder // The sort order for the returned rows
)
Table 2 shows how the arguments to query(Uri,projection,selection,selectionArgs,sortOrder)
match a SQL SELECT statement:
query() argument | SELECT keyword/parameter | Notes |
---|---|---|
Uri | FROM table_ | Uri maps to the table in the provider named table_name. |
projection | col, | projection is an array of columns that is included for each row retrieved. |
selection | WHERE col = value | selection specifies the criteria for selecting rows. |
selectionArgs | No exact equivalent. Selection arguments replace ? placeholders in the selection clause. | |
sortOrder | ORDER BY col, | sortOrder specifies the order in which rows appear in the returned Cursor . |
Content URIs
A content URI is a URI that identifies data in a provider. Content URIs include the symbolic name of the entire provider—its authority—and a name that points to a table—a path. When you call a client method to access a table in a provider, the content URI for the table is one of the arguments.
In the preceding lines of code, the constant CONTENT_URI
contains the content URI of the User Data Provider’s Users table. The Content Resolver
object parses out the URI’s authority and uses it to resolve the provider by comparing the authority to a system table of known providers. The Content Resolver
can then dispatch the query arguments to the correct provider.
The Content Provider
uses the path part of the content URI to choose the table to access. A provider usually has a path for each table it exposes.
In the previous lines of code, the full URI for the Users
table is:
content://com.example.learngit/users
- The
content://
string is the scheme, which is always present and identifies this as a content URI. - The
com.example.learngit
string is the provider’s authority(normally the package name of the provide app). - The
users
string is the table’s path.
Many providers let you access a single row in a table by appending an ID value to the end of the URI. For example, to retrieve a row whose _ID
is 4
from the User Dictionary Provider, you can use this content URI:
val singleUri: Uri = ContentUris.withAppendedId(CONTENT_URI, 4)
Now write the actual implementation of our query function to fetch data from the data base.
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
var sortOrder = sortOrder
val qb = SQLiteQueryBuilder()
qb.tables = TABLE_NAME
when (uriMatcher!!.match(uri)) {
uriCode -> qb.projectionMap = values
else -> throw IllegalArgumentException("Unknown URI $uri")
}
if (sortOrder == null || sortOrder === "") {
sortOrder = id
}
val c = qb.query(
db, projection, selection, selectionArgs, null,
null, sortOrder
)
c.setNotificationUri(context!!.contentResolver, uri)
return c
}
Now similarly write code for other 3 functions namely insert, update and delete
// adding data to the database
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val rowID = db!!.insert(TABLE_NAME, "", values)
if (rowID > 0) {
val _uri = ContentUris.withAppendedId(CONTENT_URI, rowID)
context!!.contentResolver.notifyChange(_uri, null)
return _uri
}
throw SQLiteException("Failed to add a record into $uri")
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
var count = 0
count = when (uriMatcher!!.match(uri)) {
uriCode -> db!!.update(TABLE_NAME, values, selection, selectionArgs)
else -> throw IllegalArgumentException("Unknown URI $uri")
}
context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
var count = 0
count = when (uriMatcher!!.match(uri)) {
uriCode -> db!!.delete(TABLE_NAME, selection, selectionArgs)
else -> throw IllegalArgumentException("Unknown URI $uri")
}
context!!.contentResolver.notifyChange(uri, null)
return count
}
Now one last function left named getType
MIME types for tables
The getType()
method returns a String
in MIME format that describes the type of data returned by the content URI argument. The Uri
argument can be a pattern rather than a specific URI. In this case, return the type of data associated with content URIs that match the pattern. For common types of data such as text, HTML, or JPEG, getType()
returns the standard MIME type for that data.
For content URIs that point to a row or rows of table data, getType()
returns a MIME type in Android’s vendor-specific MIME format:
- Type part:
vnd
- Subtype part:
- If the URI pattern is for a single row:
android.cursor.item/
- If the URI pattern is for more than one row:
android.cursor.dir/
- If the URI pattern is for a single row:
- Provider-specific part:
vnd.<name>
.<type>
You supply the<name>
and<type>
. The<name>
value is globally unique, and the<type>
value is unique to the corresponding URI pattern. A good choice for<name>
is your company’s name or some part of your application’s Android package name. A good choice for the<type>
is a string that identifies the table associated with the URI.
For example, if a provider’s authority is com.example.app.provider
, and it exposes a table named table1
, the MIME type for multiple rows in table1
is:
vnd.android.cursor.dir/vnd.com.example.provider.table1
For a single row of table1
, the MIME type is:
vnd.android.cursor.item/vnd.com.example.provider.table1
override fun getType(uri: Uri): String? {
return when (uriMatcher!!.match(uri)) {
uriCode -> "vnd.android.cursor.dir/users"
else -> throw IllegalArgumentException("Unsupported URI: $uri")
}
}
After this don’t forget to add the provider in the app’s manifest file
<provider
android:name=".phase2.MyContentProvider"
android:authorities="com.example.learngit"
android:enabled="true"
android:exported="true"
android:permission="com.example.learngit.ACCESS_USERS"/>
If you want to add permission to access you content provider then define it in manifest file
<permission
android:name="com.example.learngit.ACCESS_USERS"
android:protectionLevel="normal" />
Time to access our Provider in other apps
We have successfully created our content provider to manage user data in our app. now we will access the data in other app as we usually do in our apps like access contacts from the content provider of Contacts app.
First thing first define the same permission in our app to access the data of provider app. The <queries>
element is required to explicitly declare the packages or components your app needs to interact with. Starting from Android 11 (API 30), Android introduced package visibility restrictions to enhance privacy. This means:
Adding a <queries>
declaration informs the system that your app will interact with a specific app or its content provider.
Apps can no longer query other apps or their components without declaring their intent in the manifest.
<uses-permission android:name="com.example.learngit.ACCESS_USERS" />
<queries>
<package android:name="com.example.learngit"/>
</queries>
Now simply user this UI in compose to display data from the content Provider.
@Composable
fun UserListScreen(contentResolver: ContentResolver) {
// defining authority so that other application can access it
val PROVIDER_NAME = "com.example.learngit"
// defining content URI
val URL = "content://$PROVIDER_NAME/users"
// parsing the content URI
val CONTENT_URI = Uri.parse(URL)
// State to hold list of users
val users = remember { mutableStateListOf<Map<String, String>>() }
// States for input fields
var name by remember { mutableStateOf("") }
var userIdToUpdate by remember { mutableStateOf<Int?>(null) }
// Function to fetch all users
fun fetchUsers() {
val cursor = contentResolver.query(
CONTENT_URI,
null, // Projection
null, // Selection
null, // Selection args
null // Sort order
)
users.clear()
cursor?.use {
while (it.moveToNext()) {
val id = it.getInt(it.getColumnIndexOrThrow("id"))
val name = it.getString(it.getColumnIndexOrThrow("name"))
users.add(mapOf("id" to id.toString(), "name" to name))
}
}
}
// Insert or Update function
fun saveUser() {
if (name.isNotBlank()) {
val values = ContentValues().apply {
put("name", name)
}
if (userIdToUpdate != null) {
val selection = "id = ?"
val selectionArgs = arrayOf(userIdToUpdate.toString())
contentResolver.update(
CONTENT_URI,
values, selection, selectionArgs
)
userIdToUpdate = null
} else {
contentResolver.insert(CONTENT_URI, values)
}
name = ""
fetchUsers()
}
}
// Delete function
fun deleteUser(id: Int) {
val selection = "id = ?"
val selectionArgs = arrayOf(id.toString())
contentResolver.delete(CONTENT_URI, selection, selectionArgs)
fetchUsers()
}
// Initial data fetch
LaunchedEffect(Unit) {
fetchUsers()
}
// UI
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Input fields
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { saveUser() },
modifier = Modifier.fillMaxWidth()
) {
Text(if (userIdToUpdate != null) "Update User" else "Add User")
}
Spacer(modifier = Modifier.height(16.dp))
// User list
LazyColumn {
items(users) { user ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text("ID: ${user["id"]}")
Text("Name: ${user["name"]}")
}
Row {
IconButton(onClick = {
name = user["name"].orEmpty()
userIdToUpdate = user["id"]?.toInt()
}) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Edit"
)
}
IconButton(onClick = {
deleteUser(user["id"]!!.toInt())
}) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete"
)
}
}
}
}
}
}
}
GitHub Gist link:- here
Pingback: Read & Write Call Logs in Android using Jetpack compose | Content Providers | Android Storage – decodeandroid.com
Pingback: Read & Write Contacts in Android using Jetpack Compose – decodeandroid.com