学习了运行时权限,接下来就进入内容提供器的学习
什么是内容提供器
Android 的内容提供器(Content Provider)是一种组件,用于管理应用程序之间共享的结构化数据。内容提供器允许应用程序将数据暴露给其他应用程序,从而实现数据共享和交互。
内容提供器的主要功能包括:
-
数据存储和检索:内容提供器可以用于存储和检索持久化数据,例如数据库、文件系统等。它提供了一组标准的 CRUD(创建、读取、更新、删除)操作,允许其他应用程序通过 URI 和内容解析器访问数据。
-
数据共享:内容提供器允许应用程序共享数据,使其他应用程序可以访问和使用这些数据。通过定义适当的 URI 和权限,应用程序可以控制对数据的访问级别。
-
跨应用数据交互:内容提供器促进了应用程序之间的数据交互。一个应用程序可以通过内容提供器共享数据,而另一个应用程序可以通过内容解析器查询、插入、更新或删除这些数据。
-
数据权限控制:内容提供器可以定义不同级别的访问权限,以确保只有具有适当权限的应用程序可以访问和修改数据。这有助于保护敏感数据并维护数据的安全性。
使用内容提供器时,开发人员需要定义合适的数据URI、URI匹配规则和权限。此外,还需要实现一组标准的方法,以处理查询、插入、更新和删除操作。
Android 提供了一些默认的内容提供器,如联系人、媒体库、日历等,开发人员也可以自定义内容提供器来管理应用程序的数据。
总结起来,Android 的内容提供器是一种强大的组件,用于管理和共享应用程序之间的结构化数据。它促进了数据共享和交互,并提供了一种标准的接口和权限控制机制,以确保数据的安全性和访问控制。
ContentResolver的基本用法
1、对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要
借助ContentResolver类,可以通过Context中的getContentResolver()方法获取到该类的实例。ContenResolver中提供了系列的方法用于对数据进行CRUD操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。
2、不同于SQLiteDatabase,ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成: authority 和path。 authority 是用于对不同的应用程序做区分的,一般为了 避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的authority就可以命名为com.example.app. provider。path 则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。
比如某个程序的数据库里存在两张表: tablel和table2,这时就可以将path分别命名为/tablel和/table2,然后把authority和path进行组合,内容URI就变成了com.example app povider/tablel和com. example. app.pprovider/table2。
不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:
content://com. example. app. provider/table1
content://com. example. app. provider/table2
3、有没有发现,内容URI可以非常清楚地表达出我们]想要访问哪个程序中哪张表里的数据。也正是因此,ContentResolver 中的增删改查方法才都接收Uri对象作为参数,因为如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。
在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入,解析的方法如下:
Uri uri = Uri.parse("content://com.example.app.provider/table")
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
现在我们就可以使用这个Uri对象来查询tabel表中的数据了,代码如下:
Cursor cursor = getContentResolver()
.query(uri,projection,selection,selectionArgs,sortOder);
添加、更新、删除数据
向table表中添加一条数据
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2","text");
getContentResolver().insert(uri,values);
可以看到,仍然是将添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。
更新
现在如果我们想要更新这条新添加的数据,把column1的值清空,可以借助ContentResolver的update()方法实现,代码如下:
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ? ",new String[] {"text","1"});
删除
注意上述代码使用了selection和selectionArgs参数来对象要更新的数据进行约束,以防止所有的行都会受影响。最后,可以调用ContentResolver的delete()方法将这条数据删除掉,代码如下:
getContentResolver().delete(Uri,"column2 = ? ",new String[] {"1"});
创建自己的内容提供器的步骤
如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器,ContentProvider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写,新建MyProvider继承自ContentProvider
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public String getType(Uri uri) {
return null;
}
}
onCreate()
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false则表示失败。
query()
从内容提供器中查询数据。使用 uri 参数来确定查询哪张表, projection 参数用于确定查询哪些列, selection和selectionArgs 参数用于约束查询哪些行, sortOrder 参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
insert()
向内容提供器中添加一条数据。使用 uri 参数来确定要添加到的表,待添加的数据保存在 values 参数中。添加完成后,返回一个用于表示这条新记录的URI。
update()
更新内容提供器中已有的数据。使用 uri 参数来确定更新哪一张表中的数据,新数据保存在 values 参数中, selection和selectionArgs 参数用于约束更新哪些行,受影响的行数将作为返回值返回。
delete()
从内容提供器中删除数据。使用 uri 参数来确定删除哪一张表中的数据, selection和selectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回。
getType()
根据传入的内容 URI 来返回相应的 MIME 类型。
一个标准的内容URI写法是这样的:
content://com.example.app.provider/table1
还可以在这个内容URI的后面加上一个id,表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据:
content://com.example.app.provider/table1/1
可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下:
*:表示匹配任意长度的任意 字符。
#:表示匹配任意长度的 数字。
一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*
一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#
接着,再借助 UriMatcher 这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个 addURI() 方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,可以判断出调用方期望访问的是哪张表中的数据了。修改MyProvider中的代码:
public class MyProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//TABLE1_DIR表示访问table1表中的所有数据
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
//TABLE1_ITEM表示访问table1表中的单条数据
uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
//TABLE2_DIR表示访问table2表中的所有数据
uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);
//TABLE2_ITEM表示访问table2表中的单条数据
uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);
}
...
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
// 查询table1表中的所有数据
break;
case TABLE1_ITEM:
// 查询table1表中的单条数据
break;
case TABLE2_DIR:
// 查询table2表中的所有数据
break;
case TABLE2_ITEM:
// 查询table2表中的单条数据
break;
default:
break;
}
...
}
...
}
接着在静态代码块里我们创建了UriMatcher的实例,并调用addURI()方法,将期望匹配的内容URI格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当query()方法被调用的时候,就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。insert()、update()、delete()这几个方法的实现也是差不多的,它们都会携带Uri这个参数,然后同样利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,
- 必须以 vnd 开头。
- 如果 内容 URI 以路径结尾,则后接
android.cursor.dir/
,如果内容 URI 以 id 结尾 ,则后接android.cursor.item/
。 - 最后接上
vnd.<authority>.<path>
。
对于content://com.example.app.provider/table1这个内容URI,它所对应的MIME类型就可以写成:
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
对于content://com.example.app.provider/table1/1这个内容URI,它所对应的MIME类型就可以写成:
vnd.android.cursor.item/vnd.com.example.app.provider.table1
继续完善MyProvider中的内容了,这次来实现getType()方法中的逻辑,
public class MyProvider extends ContentProvider {
...
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
default:
break;
}
return null;
}
}
如何才能保证隐私数据不会泄漏出去呢?所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
实现跨程序数据共享
创建一个内容提供器,右击com.example.databasetest包→New→Other→Content Provider。将内容提供器命名为DatabaseProvider,authority指定com.example.databasetest.provider,Exported属性表示是否允许外部程序访问我们的内容提供器,Enabled属性表示是否启用这个内容提供器。将两个属性都勾中,点击Finish完成创建。
public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
// 查询数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null,
null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[] { bookId },
null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[]
{ categoryId }, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// 更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[]
{ bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection,
selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[]
{ categoryId });
break;
default:
break;
}
return updatedRows;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category";
}
return null;
}
}
调用了Uri对象的getPathSegments()方法,它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了。
内容提供器一定要在AndroidManifest.xml文件中注册才可以使用
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.databasetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true">
</provider>
</application>
</manifest>
标签内出现了一个新的标签<provider>,使用它来对DatabaseProvider这个内容提供器进行注册。android:name 属性指定了DatabaseProvider的类名,android:authorities 属性指定了DatabaseProvider的authority,而enabled和exported属性则是根据创建的勾选状态自动生成的,这里表示允许DatabaseProvider被其他应用程序进行访问。
布局文件很简单,里面放置了4个按钮,分别用于添加、查询、修改和删除数据。然后修改MainActivity中的代码。
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 添加数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/
book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 查询数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/
book");
Cursor cursor = getContentResolver().query(uri, null, null, null,
null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor. getColumnIndex
("name"));
String author = cursor.getString(cursor. getColumnIndex
("author"));
int pages = cursor.getInt(cursor.getColumnIndex ("pages"));
double price = cursor.getDouble(cursor. 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);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 更新数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/
book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 删除数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/
book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
上述知识均摘抄自郭霖的《Android第一行代码》