Mission requirements
- One as a ContentProvider providing contact data
- Another one acts as an Observer to monitor changes in contact data:
1. Create the ContactProvider project;
2. Use the Sqlite database in the ContactProvider project to implement the reading and writing functions of contacts;
3. Provide contact data through ContentProvider in the ContactProvider project;
4. Create the ContactObserver project;
5. Register ContentObserver in the ContactObserver project to monitor changes in the contact database.
When creating an Android contact application, including a ContactProvider project and a ContactObserver project, you need to implement the add, delete, modify, and query methods as well as the layout files of the two pages. Here are the steps in more detail:
Step 1: Create ContactProvider project
-
Create a new Android project named ContactProvider.
-
In the ContactProvider project, create a Java class named ContactContract that defines the database table structure and the URI of the content provider.
package com.leo.contactprovider;
import android.net.Uri;
import android.provider.BaseColumns;
public class ContactContract {
public static final String AUTHORITY = "com.leo.contactprovider";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);
public static final String PATH_CONTACTS = "contacts";
public static final class ContactEntry implements BaseColumns {
public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_CONTACTS).build();
public static final String TABLE_NAME = "contacts";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_PHONE = "phone";
}
}}
- Create a database helper class named ContactDbHelper for creating and managing contact databases.
package com.example.contactprovider;
package com.leo.contactprovider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class ContactDbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "contacts.db";
private static final int DATABASE_VERSION = 1;
public ContactDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
final String SQL_CREATE_CONTACTS_TABLE = "CREATE TABLE " +
ContactContract.ContactEntry.TABLE_NAME + " (" +
ContactContract.ContactEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
ContactContract.ContactEntry.COLUMN_NAME + " TEXT NOT NULL, " +
ContactContract.ContactEntry.COLUMN_PHONE + " TEXT NOT NULL" +
");";
db.execSQL(SQL_CREATE_CONTACTS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + ContactContract.ContactEntry.TABLE_NAME);
onCreate(db);
}
}
- Create a content provider class named ContactProvider to implement the function of adding, deleting, modifying and checking contact data.
“New” —> “Other” —> “Cotent Provider”
package com.leo.contactprovider;
import static com.leo.contactprovider.ContactContract.AUTHORITY;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ContactProvider extends ContentProvider {
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Define integer constants for the URIs
private static final int CONTACTS = 100;
private static final int CONTACT_ID = 101;
static {
uriMatcher.addURI(AUTHORITY, "contacts", CONTACTS);
uriMatcher.addURI(AUTHORITY, "contacts/#", CONTACT_ID);
}
private ContactDbHelper dbHelper;
@Override
public boolean onCreate() {
dbHelper = new ContactDbHelper(getContext());
return true;
}
// 实现数据的增删改查方法
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(ContactContract.ContactEntry.TABLE_NAME, null, values);
if (id > 0) {
getContext().getContentResolver().notifyChange(uri, null);
return ContactContract.ContactEntry.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
}
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int rowsDeleted;
switch (uriMatcher.match(uri)) {
case CONTACTS:
rowsDeleted = db.delete(ContactContract.ContactEntry.TABLE_NAME, selection, selectionArgs);
break;
case CONTACT_ID:
String contactId = uri.getLastPathSegment();
rowsDeleted = db.delete(ContactContract.ContactEntry.TABLE_NAME,
ContactContract.ContactEntry._ID + "=?", new String[]{
contactId});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (rowsDeleted > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsDeleted;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int rowsUpdated;
switch (uriMatcher.match(uri)) {
case CONTACTS:
rowsUpdated = db.update(ContactContract.ContactEntry.TABLE_NAME, values, selection, selectionArgs);
break;
case CONTACT_ID:
String contactId = uri.getLastPathSegment();
rowsUpdated = db.update(ContactContract.ContactEntry.TABLE_NAME, values,
ContactContract.ContactEntry._ID + "=?", new String[]{
contactId});
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if (rowsUpdated > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsUpdated;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
switch (uriMatcher.match(uri)) {
case CONTACTS:
cursor = db.query(ContactContract.ContactEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
break;
case CONTACT_ID:
String contactId = uri.getLastPathSegment();
cursor = db.query(ContactContract.ContactEntry.TABLE_NAME, projection,
ContactContract.ContactEntry._ID + "=?", new String[]{
contactId}, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// Set notification URI on the cursor
cursor.setNotificationUri(getContext().getContentResolver(), uri); // 添加这行代码
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
// ...
}
-
UriMatcher:
UriMatcher
is used to match incoming URIs to determine which operation to perform (for example, query, insert, delete, or update) . Your code initializesuriMatcher
using a static initialization block and assigns constant integers to two different types of URIs ("contacts" and "contacts/#"). -
onCreate method: This is the initialization method of the content provider. Here, you initialize the database helper class (
ContactDbHelper
) and associate it with the content provider. Content provider initialization is done when the application starts. -
insert method: This is the method used to insert data. When an application inserts new data through a content provider, the
insert
method opens the database, performs the insertion, and notifies any content observers interested in the data usingnotifyChange
. -
delete method: This method is used to delete data. It checks the URI and then performs the delete operation based on the type of URI. If some rows are successfully deleted, it notifies content observers using
notifyChange
. -
update method: used to update data. Similar to the
delete
method, it checks the URI, performs the appropriate update operation, and notifies content observers usingnotifyChange
. -
query method: used to query data. This is the most common method used by content providers to retrieve data. It performs a query based on the incoming URI and then notifies the associated content observer using
setNotificationUri
. -
setNotificationUri: This method associates query results with a specific URI to notify observers when data changes.
This code covers the core functionality of the content provider, including handling insert, delete, update, and query operations, as well as notifying relevant content observers when these operations are completed. Content providers allow applications to share data and enable real-time data updates through the content observer pattern.
- Create layout file -
contact_provider
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Contact"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" />
<Button
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete Contact"
android:layout_below="@+id/btnAdd"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" />
<Button
android:id="@+id/btnUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Update Contact"
android:layout_below="@+id/btnDelete"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" />
<Button
android:id="@+id/btnQuery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Query Contacts"
android:layout_below="@+id/btnUpdate"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/textViewResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btnQuery"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="16dp"
android:textSize="20sp"
/>
</RelativeLayout>
achieve effect
Add the appropriate functionality for each button in the ContactProvider
project.
package com.leo.contactprovider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private TextView textViewResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contact_provider);
textViewResult = findViewById(R.id.textViewResult);
Button btnAdd = findViewById(R.id.btnAdd);
Button btnDelete = findViewById(R.id.btnDelete);
Button btnUpdate = findViewById(R.id.btnUpdate);
Button btnQuery = findViewById(R.id.btnQuery);
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 添加联系人示例
ContentValues values = new ContentValues();
values.put(ContactContract.ContactEntry.COLUMN_NAME, "John Doe");
values.put(ContactContract.ContactEntry.COLUMN_PHONE, "123-456-7890");
Uri insertUri = getContentResolver().insert(ContactContract.ContactEntry.CONTENT_URI, values);
textViewResult.setText("Contact added with URI: " + insertUri.toString());
}
});
btnDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 删除联系人示例
String selection = ContactContract.ContactEntry.COLUMN_NAME + " = ?";
String[] selectionArgs = {
"John Doe"};
int deletedRows = getContentResolver().delete(ContactContract.ContactEntry.CONTENT_URI, selection, selectionArgs);
textViewResult.setText("Deleted " + deletedRows + " contacts.");
}
});
btnUpdate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 更新联系人示例
ContentValues values = new ContentValues();
values.put(ContactContract.ContactEntry.COLUMN_PHONE, "987-654-3210");
String selection = ContactContract.ContactEntry.COLUMN_NAME + " = ?";
String[] selectionArgs = {
"John Doe"};
int updatedRows = getContentResolver().update(ContactContract.ContactEntry.CONTENT_URI, values, selection, selectionArgs);
textViewResult.setText("Updated " + updatedRows + " contacts.");
}
});
btnQuery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 查询联系人示例
String[] projection = {
ContactContract.ContactEntry._ID,
ContactContract.ContactEntry.COLUMN_NAME,
ContactContract.ContactEntry.COLUMN_PHONE
};
Cursor cursor = getContentResolver().query(ContactContract.ContactEntry.CONTENT_URI, projection, null, null, null);
StringBuilder result = new StringBuilder();
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactContract.ContactEntry.COLUMN_NAME));
String phone = cursor.getString(cursor.getColumnIndexOrThrow(ContactContract.ContactEntry.COLUMN_PHONE));
result.append("Name: ").append(name).append(", Phone: ").append(phone).append("\n");
}
cursor.close();
textViewResult.setText(result.toString());
}
});
}
}
Overall implementation effect
Step 2: Create ContactObserver project
-
Create a new Android project and name it ContactObserver.
-
Create
activity_main.xml
layout file to display the monitored content.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/textViewObserver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Content Observer Output" />
</RelativeLayout>
- Create
MainActivity.java
to display the monitored content.
package com.leo.contactobserver;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class ContactObserverActivity extends AppCompatActivity {
private TextView textViewObserver;
private ContentObserver contentObserver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textViewObserver = findViewById(R.id.textViewObserver);
// 创建 ContentObserver 实例
contentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
// 处理内容变化时的逻辑
Log.i("Content Changed: ", "URI: " + uri.toString());
Log.i("Content Changed: ", "Self Change: " + selfChange);
// 你可以在这里更新 textViewObserver 中的内容
textViewObserver.setText("Content Changed: " + uri.toString());
}
};
}
@Override
protected void onResume() {
super.onResume();
// 注册 ContentObserver 监听内容变化
getContentResolver().registerContentObserver(
Uri.parse("content://com.leo.contactprovider/contacts"),
true, contentObserver);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 在活动销毁时取消注册 ContentObserver
getContentResolver().unregisterContentObserver(contentObserver);
}
}
Here are the key parts explained and highlighted:
-
ContentObserver:
ContentObserver
is a class used to monitor data changes on a specific URI. In the code, we create an instance ofContentObserver
to monitor data changes of a specific content provider. -
onChange method:
onChange
method is the callback method ofContentObserver
, when the data of the monitored URI changes. is called. Within this method, you can handle the logic of data changes. In the code, the change of URI and the value of are recorded throughLog
.selfChange
-
onResume method: In the
onResume
method,ContentObserver
is registered to listen for content changes on a specific URI . This way,ContentObserver
will only take effect when the activity is in the foreground. -
onDestroy method: In the
onDestroy
method, the registration ofContentObserver
is cancelled. Because there is no need to listen for content changes when the activity is destroyed. By unregistering, you avoid potential memory leaks.
Working principle:
- When the content provider's data changes, the content provider will internally call the
ContentResolver
'snotifyChange
method and notify all registered listeners /span>ContentObserver
. ContentObserver
will trigger theonChange
callback method, which includes the URI of the change and a flag indicating whether the change was initiated by itself (selfChange
).- You can handle data changes in the
onChange
method, such as updating the UI or recording logs.
- Modify configuration file
Add query listening authorities
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
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.AppCompat.DayNight"
tools:targetApi="31">
<activity
android:name=".ContactObserverActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.DayNight">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<queries>
<provider android:authorities="com.leo.contactprovider"
android:exported="true" />
</queries>
</manifest>
Make sure your ContactObserverService
properly registers and unregisters ContentObserver, and sends and receives broadcast messages.
achieve effect
The provider modifies the data, and the observer monitors the data modification.