Android - Content Provider
Android - Content Providers
Section titled “Android - Content Providers”一个 ContentProvider 管理对中央数据存储库的访问。它是一个标准接口,用于连接一个应用中的数据与另一个应用中运行的代码。Content Provider 是 Android 应用的主要构建块之一,为数据存储和检索提供了一个抽象层。
Content Provider 的主要功能和目的:
- 数据共享:它们允许应用程序在权限允许的情况下与其他应用程序共享其数据。
- 抽象:它们抽象了底层数据源(例如,SQLite 数据库、文件、网络)。消费应用程序不需要知道数据如何存储。
- 安全性:它们提供了一种受控的数据访问方式,通常需要特定的权限。
- 标准接口:它们使用基于 URI 的通用方案和一组 CRUD(创建、读取、更新、删除)方法。
虽然你可以创建自己的 Content Provider 来分享你的应用数据,但你更常会与 Android 系统自带的 Content Provider(例如,Contacts, MediaStore)或其他第三方应用提供的 Content Provider 进行交互。
当一个应用想访问来自 Content Provider 的数据时,它会使用其 Context 中的 ContentResolver 对象向 Provider 发送请求。然后 ContentProvider 执行请求的操作并返回结果。
Content URIs
Section titled “Content URIs”每个 Content Provider 通过一个公共 URI(统一资源标识符)暴露其数据,该 URI 唯一标识数据集。Content URI 的一般格式如下:
content://<authority>/<path>/<optional_id>URI 组件解析:
| 部分 | 描述 |
|---|---|
content:// | 方案(Scheme)。始终存在,表示这是一个 content URI。 |
<authority> | 一个标识 Content Provider 的符号名称。对于 Android 内置的 Provider,这通常是一个众所周知的字符串(例如,com.android.contacts)。对于第三方应用,它通常是应用的 Package Name 加上一个唯一的 Provider 标识符(例如,com.example.myapp.provider)。 |
<path> | 一个指示所请求数据类型(例如,特定的表名,如 contacts 或 images)的字符串。一个 Provider 可以处理多个路径。 |
<optional_id> | 路径中特定记录的数字标识符。如果存在,URI 请求单个记录。如果不存在,通常请求路径中的所有记录。 |
示例:content://com.android.contacts/contacts 可能指代所有联系人,而 content://com.android.contacts/contacts/123 可能指代 ID 为 123 的联系人。
创建 Content Provider
Section titled “创建 Content Provider”虽然自定义 Content Provider 对于应用内部数据(通常更倾向于使用 Jetpack Room 或 DataStore)来说频率较低,但如果你需要安全地将应用数据暴露给其他应用程序,它就至关重要。
创建 Content Provider 的步骤:
- 设计数据存储:决定如何存储数据(例如,SQLite 数据库、文件)。
- 扩展
ContentProvider:创建一个扩展android.content.ContentProvider的类。 - 实现核心方法:重写以下抽象方法:
- 定义 Content URIs:设计你的 Provider 将响应的 URI。通常使用
UriMatcher来解析传入的 URI。 - 在 Manifest 中声明:在
AndroidManifest.xml文件中使用<provider>标签注册你的ContentProvider,指定其 Authority 和权限。
需要实现的 ContentProvider 关键方法:
public boolean onCreate(): 在 Provider 启动时调用。在此执行初始化操作,例如设置数据库连接。如果 Provider 成功加载,返回true。public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder): 返回数据给调用者。uri指定要查询的数据。projection列出要返回的列。selection和selectionArgs定义 WHERE 子句。sortOrder定义 ORDER BY 子句。应返回一个Cursor或抛出异常。public Uri insert(Uri uri, ContentValues values): 向 Provider 中插入新行。values包含新行的数据。应返回新插入行的 URI。public int delete(Uri uri, String selection, String[] selectionArgs): 从 Provider 中删除行。应返回删除的行数。public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs): 更新 Provider 中的现有行。应返回更新的行数。public String getType(Uri uri): 返回与 content URI 相关联的数据的 MIME 类型。对于一个项目目录,通常是ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.your.authority.your_path"。对于单个项目,则是ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.your.authority.your_path"。
示例:一个用于Notes的简单自定义Content Provider
Section titled “示例:一个用于Notes的简单自定义Content Provider”本示例概述了如何使用 SQLite 数据库创建一个基本的 ContentProvider 来存储简单的 Notes。
| 步骤 | 描述 |
|---|---|
| 1 | 创建一个新的 Android Studio 项目(MyContentProviderApp,包名为 com.example.mycontentproviderapp)。 |
| 2 | 定义一个用于数据库 Schema 和 URI 的 Contract 类(NotesContract.java)。 |
| 3 | 创建一个 SQLiteOpenHelper 子类用于数据库管理(NotesDbHelper.java)。 |
| 4 | 实现 ContentProvider 子类(MyNotesProvider.java)。 |
| 5 | 在 AndroidManifest.xml 中声明 Provider。 |
| 6 | 修改 MainActivity.java 及其布局以与 Provider 交互(添加、查询 Notes)。 |
NotesContract.java(定义 URI 和数据库 Schema 常量):
package com.example.mycontentproviderapp;
import android.net.Uri;import android.provider.BaseColumns;
public final class NotesContract { private NotesContract() {}
public static final String AUTHORITY = "com.example.mycontentproviderapp.provider"; public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); public static final String PATH_NOTES = "notes";
public static final class NoteEntry implements BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_NOTES).build();
public static final String TABLE_NAME = "notes"; public static final String COLUMN_TITLE = "title"; public static final String COLUMN_CONTENT = "content";
// MIME types public static final String CONTENT_TYPE_DIR = "vnd.android.cursor.dir/" + AUTHORITY + "/" + PATH_NOTES; public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/" + AUTHORITY + "/" + PATH_NOTES; }}NotesDbHelper.java(管理 SQLite 数据库):
package com.example.mycontentproviderapp;
import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;
public class NotesDbHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "notes.db"; private static final int DATABASE_VERSION = 1;
public NotesDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); }
@Override public void onCreate(SQLiteDatabase db) { final String SQL_CREATE_NOTES_TABLE = "CREATE TABLE " + NotesContract.NoteEntry.TABLE_NAME + " (" + NotesContract.NoteEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + NotesContract.NoteEntry.COLUMN_TITLE + " TEXT NOT NULL, " + NotesContract.NoteEntry.COLUMN_CONTENT + " TEXT NOT NULL);"; db.execSQL(SQL_CREATE_NOTES_TABLE); }
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + NotesContract.NoteEntry.TABLE_NAME); onCreate(db); }}MyNotesProvider.java(ContentProvider 实现 - 简化版):
package com.example.mycontentproviderapp;
import android.content.ContentProvider;import android.content.ContentUris;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 MyNotesProvider extends ContentProvider {
private NotesDbHelper dbHelper; private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int NOTES = 100; // For all notes private static final int NOTE_ID = 101; // For a single note by ID
static { uriMatcher.addURI(NotesContract.AUTHORITY, NotesContract.PATH_NOTES, NOTES); uriMatcher.addURI(NotesContract.AUTHORITY, NotesContract.PATH_NOTES + "/#", NOTE_ID); }
@Override public boolean onCreate() { dbHelper = new NotesDbHelper(getContext()); return true; }
@Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { SQLiteDatabase database = dbHelper.getReadableDatabase(); Cursor cursor; int match = uriMatcher.match(uri);
switch (match) { case NOTES: cursor = database.query(NotesContract.NoteEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); break; case NOTE_ID: selection = NotesContract.NoteEntry._ID + "=?"; selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))}; cursor = database.query(NotesContract.NoteEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); break; default: throw new IllegalArgumentException("Cannot query unknown URI " + uri); } // Set notification URI on the Cursor, so it knows what content URI to watch for changes if (getContext() != null) { cursor.setNotificationUri(getContext().getContentResolver(), uri); } return cursor; }
@Nullable @Override public String getType(@NonNull Uri uri) { final int match = uriMatcher.match(uri); switch (match) { case NOTES: return NotesContract.NoteEntry.CONTENT_TYPE_DIR; case NOTE_ID: return NotesContract.NoteEntry.CONTENT_TYPE_ITEM; default: throw new IllegalStateException("Unknown URI " + uri + " with match " + match); } }
@Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { final int match = uriMatcher.match(uri); if (match == NOTES) { SQLiteDatabase database = dbHelper.getWritableDatabase(); long id = database.insert(NotesContract.NoteEntry.TABLE_NAME, null, values); if (id == -1) { // Log.e(TAG, "Failed to insert row for " + uri); return null; } if (getContext() != null) { getContext().getContentResolver().notifyChange(uri, null); } return ContentUris.withAppendedId(uri, id); } throw new IllegalArgumentException("Insertion is not supported for " + uri); }
@Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { SQLiteDatabase database = dbHelper.getWritableDatabase(); int rowsDeleted; final int match = uriMatcher.match(uri); switch (match) { case NOTES: rowsDeleted = database.delete(NotesContract.NoteEntry.TABLE_NAME, selection, selectionArgs); break; case NOTE_ID: selection = NotesContract.NoteEntry._ID + "=?"; selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))}; rowsDeleted = database.delete(NotesContract.NoteEntry.TABLE_NAME, selection, selectionArgs); break; default: throw new IllegalArgumentException("Deletion is not supported for " + uri); } if (rowsDeleted != 0 && getContext() != null) { getContext().getContentResolver().notifyChange(uri, null); } return rowsDeleted; }
@Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { final int match = uriMatcher.match(uri); switch (match) { case NOTES: // For multiple rows, selection and selectionArgs are used directly. return updateNote(uri, values, selection, selectionArgs); case NOTE_ID: // For a single row, modify selection to target the specific ID from the URI. selection = NotesContract.NoteEntry._ID + "=?"; selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))}; return updateNote(uri, values, selection, selectionArgs); default: throw new IllegalArgumentException("Update is not supported for " + uri); } }
private int updateNote(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (values == null || values.size() == 0) { return 0; } SQLiteDatabase database = dbHelper.getWritableDatabase(); int rowsUpdated = database.update(NotesContract.NoteEntry.TABLE_NAME, values, selection, selectionArgs); if (rowsUpdated != 0 && getContext() != null) { getContext().getContentResolver().notifyChange(uri, null); } return rowsUpdated; }}AndroidManifest.xml(在<application>内声明 Provider):
<provider android:name=".MyNotesProvider" android:authorities="com.example.mycontentproviderapp.provider" android:exported="false" /> <!-- Set android:exported="true" and define permissions if you want other apps to access it -->MainActivity.java用于交互(插入和查询的简化示例):
package com.example.mycontentproviderapp;
import androidx.appcompat.app.AppCompatActivity;import android.content.ContentValues;import android.database.Cursor;import android.net.Uri;import android.os.Bundle;import android.util.Log;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private EditText editTextTitle, editTextContent; private TextView textViewNotes;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
editTextTitle = findViewById(R.id.editTextTitle); editTextContent = findViewById(R.id.editTextContent); textViewNotes = findViewById(R.id.textViewNotes); Button buttonAddNote = findViewById(R.id.buttonAddNote); Button buttonLoadNotes = findViewById(R.id.buttonLoadNotes);
buttonAddNote.setOnClickListener(v -> addNote()); buttonLoadNotes.setOnClickListener(v -> loadNotes()); }
private void addNote() { String title = editTextTitle.getText().toString().trim(); String content = editTextContent.getText().toString().trim();
if (title.isEmpty() || content.isEmpty()) { Toast.makeText(this, "Title and content cannot be empty", Toast.LENGTH_SHORT).show(); return; }
ContentValues values = new ContentValues(); values.put(NotesContract.NoteEntry.COLUMN_TITLE, title); values.put(NotesContract.NoteEntry.COLUMN_CONTENT, content);
// IMPORTANT: Database operations should be done on a background thread in a real app. // For simplicity, this example does it on the main thread. try { Uri newUri = getContentResolver().insert(NotesContract.NoteEntry.CONTENT_URI, values); if (newUri != null) { Toast.makeText(this, "Note added: " + newUri.toString(), Toast.LENGTH_LONG).show(); editTextTitle.setText(""); editTextContent.setText(""); } else { Toast.makeText(this, "Error adding note", Toast.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e(TAG, "Error inserting note", e); Toast.makeText(this, "Error inserting note: " + e.getMessage(), Toast.LENGTH_LONG).show(); } }
private void loadNotes() { // IMPORTANT: Querying should also be on a background thread (e.g., using CursorLoader or coroutines). Cursor cursor = null; try { cursor = getContentResolver().query(NotesContract.NoteEntry.CONTENT_URI, null, // Projection (null for all columns) null, // Selection null, // Selection args null // Sort order );
StringBuilder notesBuilder = new StringBuilder(); if (cursor != null && cursor.moveToFirst()) { int titleColumnIndex = cursor.getColumnIndex(NotesContract.NoteEntry.COLUMN_TITLE); int contentColumnIndex = cursor.getColumnIndex(NotesContract.NoteEntry.COLUMN_CONTENT); int idColumnIndex = cursor.getColumnIndex(NotesContract.NoteEntry._ID);
do { long id = (idColumnIndex != -1) ? cursor.getLong(idColumnIndex) : -1; String title = (titleColumnIndex != -1) ? cursor.getString(titleColumnIndex) : "N/A"; String content = (contentColumnIndex != -1) ? cursor.getString(contentColumnIndex) : "N/A"; notesBuilder.append("ID: ").append(id) .append("\nTitle: ").append(title) .append("\nContent: ").append(content) .append("\n\n"); } while (cursor.moveToNext()); textViewNotes.setText(notesBuilder.toString()); } else { textViewNotes.setText("No notes found."); } } catch (Exception e) { Log.e(TAG, "Error loading notes", e); textViewNotes.setText("Error loading notes: " + e.getMessage()); } finally { if (cursor != null) { cursor.close(); } } }}一个简单的 res/layout/activity_main.xml 将包含用于标题和内容的 EditText 字段,用于添加和加载笔记的 Button,以及一个 TextView 用于显示加载的笔记。
重要注意事项
Section titled “重要注意事项”- 线程:所有
ContentProvider方法(query、insert、update、delete)都可能从任何线程调用。确保数据库操作是线程安全的。此外,当你从 UI(Activity/Fragment)调用ContentResolver方法时,请在后台线程执行这些操作,以避免 ANR 错误。CursorLoader(尽管较旧)、Kotlin Coroutines 结合ViewModel和Room(如果不为内部数据使用 Provider),或 RxJava 都是常见的解决方案。 - 权限:如果你的 Provider 的
android:exported="true",你必须定义适当的权限(在<provider>标签中设置android:readPermission、android:writePermission以及相应的<permission>元素)来保护你的数据。 - 数据验证:在
insert()和update()中验证传入的数据,然后才将其提交到数据存储中。 - 应用内部数据的替代方案:对于不需要与其他应用共享的数据,Jetpack Room Persistence Library 通常是比创建完整的
ContentProvider更直接和现代的解决方案。 - FileProvider:为了与其他应用安全地共享文件,请使用
FileProvider,它是ContentProvider的一个特殊子类。
Content Provider 仍然是 Android 中用于应用间数据共享和访问系统数据的基础部分。理解其结构和用法对于全面的 Android 开发至关重要。