1. 序言
1.1 ContentProvider概述
内容提供器(Content Provider) 主要用于在不同的应用程序之间实现数据的共享功能,他提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还可以保证被访问的数据的安全性,目前使用内容提供器十Android实现跨程序共享数据的标准方式. 内容提供器可以选择只对哪一部分的数据进行共享,这样就可以保证我们数据的安全性.
ContentProvider的使用场景有:
- 想在自己的应用中访问别的应用,或者说一些ContentProvider暴露给我们的一些数据, 比如手机联系人,短信等!我们想对这些数据进行读取或者修改,这就需要用到ContentProvider了
- 我们自己的应用,想把自己的一些数据暴露出来,给其他的应用进行读取或操作,我们也可以用 到ContentProvider,另外我们可以选择要暴露的数据,就避免了我们隐私数据的的泄露!
不同于文件存储和SharedPreferences存储中两种全局刻度写操作模式,内容提供器只对那一部分数据进行共享。
1.1 运行时权限
Android 6.0中加入了运行时权限功能,用户不需要在安装软件时一次性授权所有申请的权限
通常的权限声明只需要在MainFest中添加要使用的权限就可以了,6.0及以后对于某些权限还需要在运行的时候在代码中检测是否有这个权限否则弹出对话框申请,拒绝的话是无法使用的.
Android将所有权限归成两类,一类是普通权限,一类是危险权限,确切还有第三类特殊权限。
Android中所有危险权限一共是9组24个权限:
非属于危险权限的,只需要在AndroidManifest.xml文件中添加权限声明即可。
进行运行时权限处理时使用的是权限名,一旦用户同意授权,该权限对应的权限组中所有的权限也会同时被授权。
示例:
- 在Android 6.0之前,若要拨打电话,修改activity_main.xml文件
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call" />
- 在MainActivity.java中构建一个隐式Intent,其action指向Intent。
ACTION_CALL
,系统内置打电话动作。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
});
}
}
- 在AndroidManifest.xml文件中声明权限:
<uses-permission android:name="android.permission.CALL_PHONE"/>
如果在android 6.0及以上系统,使用危险权限都必须进行运行时权限处理:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);;
} else {
call();
}
}
});
}
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
运行时权限处理流程步骤:
- 判断用户是否已经授权,借助
ContextCompat.checkSelfPermission()
方法,第一个参数是Context,第二个参数是具体权限名。用该方法的返回值与PackageManager.PERMISSION_GRANTED
做比较,相等说明用户已授权,不等未授权 - 已授权则直接执行逻辑操作,未授权则需要调用
ActivityCompat.requestPermission()
方法向用户申请授权,接收三个参数,第一参数Acitivity实例,第二参数是String数组,存放权限名,第三是请求码。 - 调用该申请方法,弹出权限对话框,最终回调到
onRequestPermissionResult()
方法中,授权结果封装在grantResults
参数中。这里判断一下授权结果来进行逻辑操作 - 如果检查多个权限的话,可以将要检查的权限放入数组或者集合当中,遍历检查即可
2. ContentProvider使用
内容提供器用法一般有两种,
- 一是使用现有的内容提供器读取和操作相应程序中的数据
- 另一种是创建自己的内容提供器给我们程序中的数据提供外部访问接口。系统自带的短信,电话簿,媒体库等程序都提供了类似的访问接口,我们就可以利用这个来进行再次开发和使用了.
2.1 ContentProvider基本使用
若需访问内容提供器中共享数据,一定要借助ContentResolver类,通过Context中getContentResolver()
方法获取到该类的实例。
ContentResolver类提供了一系列的方法用于对数据进行CRUD操作。不同于SQLiteDatabase,ContentResolver中的增删改查方法都不接受表名参数**,使用一个Uri参数代替,这个参数称为内容URI。
内容URI给内容提供器数据建立了唯一标识符,主要有两部分组成:authority
和path
。
- authority用于对不同的应用程序做区分的,避免冲突,都会采用程序包名方式进行命名,程序包名为com.example.app,那么程序对应的authority既可以命名为com.example.app.provider。
- path用于对同一个程序中不同的表做区分,通常添加到authority之后。数据库中存放了两张表,table1和table2,就可以将path分别命名为/table1和table2。
二者组合,内容URI就变成了:com.example.app.provider/table1和com.example.app.provider/table2
最好还需要在字符串头部加上协议声明:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
得到内容URI字符串,还需将其解析为Uri对象才可以作为参数传入,解析方法为:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
即调用Uri.parse()
方法。
- 查找
Cursor cursor = get ContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
查询完成后返回仍然是一个Cursor对象,将数据从这个对象中取出来。通过移动游标的位置来遍历Cursor所有行,然后在取出每一行中相列的数据。
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
- 添加数据
ContentValues.values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
- 更新数据
ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", "1"});
- 删除数据
getContentResolver().delete(uri, "column2 = ?", new String[] {"1"});
2.2 读取联系人
- 新建ContactsTest项目,修改
activity_main.xml
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call" />
- 修改MainActivity.java代码
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]
{ Manifest.permission.READ_CONTACTS }, 1);
} else {
readContacts();
}
}
private void readContacts() {
Cursor cursor = null;
try {
//查询联系人数据
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
//获取联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
- 修改AndroidManifest.xml文件添加权限:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
备注:
- ContactsContract.CommonDataKinds.Phone类做了封装,提供了一个CONTENT_RUI常量,这个常量就是使用
Uri.parse()
方法解析出来的结果。 -
- 联系人对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
- 手机号对应常量是:ContactsContract.CommonDataKinds.Phone.NUMBER
3. 创建内容提供器
前面学习了如何在自己的程序中访问其他应用程序的数据。只需要获取到应用程序的内容URI,然后借助ContentResolver进行CRUD操作就可以。那些提供外部访问接口的应用程序都是如何实现这种功能?怎样保证数据的安全性,使得隐私数据不会泄漏出去?
如果要实现跨程序共享数据,使用内容提供器。可以通过新建一个类去继承ContentProvider的方式去创建,该类有6个抽象方法需要重写:
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) {
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()
:初始内容提供器时调用,完成数据库创建与升级等操作。只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化query()
:从内容提供器中查询数据insert()
:向内容提供器添加数据。添加完成返回一个用于表示这条新纪录的URIupdate()
:更新内容提供器已有的数据。返回受影响的行数delete()
:从内容提供器删除数据。被删除的行数作为返回值返回getType()
根据传入的内容URI来返回相应的MIME类型
Uri有两种格式:
content://com.example.app.provider/table1/1
表示调用方期望访问的是com.example.app
这个应用的table1表中id为1的数据
content://com.example.app.provider/table1
表示调用方期望访问的是com.example.app这个应用的table1表的全部数据
内容URI
格式主要有:以路径结尾表示期望访问该表中所有数据,以id结尾表示期望访问该表中拥有相应的id数据,可以使用通配符方式匹配这两种格式的内容Uri:
*
:表示匹配任意长度的任意字符
content://com.example.app.provider/*
表示一个能够匹配任意表的内容URI格式
#
:表示匹配任意长度的数字
content://com.example.app.provider/table1/#
表示能够匹配table1中任意一行数据的内容Uri格式
UriMatcher类可以实现匹配内容URI功能。UriMatcher类中提供了一个addURI()
方法,接收三个参数:authority、path和一个自定义代码。调用UriMatcher的match()
方法,可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义码。利用这个码可以判断出调用方期望访问是哪种表中的数据。
现在简单创建一个ContentProvider:
- 在AndroidManifest.xml中进行注册,否则无法使用:
<provider
android:name=".MyProvider"
android:authorities="com.example.h.content_demo_provider"
android:enabled="true"
android:exported="true" />
第一个参数:类名
第二个参数:通常使用包名来使用,可以区分不同程序之间的内容提供器
第三个参数:启用
第四个参数:表示允许被其他的应用程序进行访问
- 创建一个继承自ContentProvider的类,并重写其中的6个方法
public class MyProvider extends ContentProvider {
public static final int BOOK_DIT = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORT_ITEM = 3;
public static UriMatcher sUriMatcher;
public static final String AUTHORITY = "com.example.h.content_demo_provider";
private MyDatabaseHelper mMyDatabaseHelper;
private SQLiteDatabase db;
{
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, "book", BOOK_DIT);
sUriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
sUriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
sUriMatcher.addURI(AUTHORITY, "category/#", CATEGORT_ITEM);
//UriMatcher 可以匹配uri 通过调用他的match()方法 匹配到就会返回我们在上面添加uri时填入的第三个参数
}
@Override
/**
* 初始化内容提供器的时候调用,通常会在这里完成对数据库的创建和升级等操作
* 返回true表示内容提供器初始化成功,返回false则表示失败.
*/
public boolean onCreate() {
//对当前内容提供器需要的资源进行初始化
mMyDatabaseHelper = new MyDatabaseHelper(getContext(), "info.db", null, 1);
db = mMyDatabaseHelper.getWritableDatabase();
Log.d(TAG, "onCreate: 内容提供器初始化完成");
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String
selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
//查询方法,通过解析uri来判断想要查询哪个程序的哪个表.通过UriMatchder进行匹配 如果有就返回前面addUri()中填入的code
Cursor cursor = null;
switch (sUriMatcher.match(uri)) {
case BOOK_DIT:
cursor = db.query("book", projection, selection, selectionArgs, null, null,
sortOrder);
Log.d(TAG, "query: 查询整个表" + cursor);
return cursor;
case BOOK_ITEM:
String itemId = uri.getPathSegments().get(1);
cursor = db.query("book", projection, "id=?", new String[]{itemId}, null, null,
sortOrder);
/**
* .getPathSegments()它会将内容URI权限之后的部分以 / 进行分割,并把分割后的结果放入到一个字符串列表中,
* 返回的列表[0]存放的就是路径,[1]存放的就是id
*/
return cursor;
case CATEGORT_ITEM:
String itemId2 = uri.getPathSegments().get(1);
cursor = db.query("category", projection, "id=?", new String[]{itemId2}, null, null,
sortOrder);
return cursor;
case CATEGORY_DIR:
cursor = db.query("category", projection, selection, selectionArgs, null, null,
sortOrder);
return cursor;
default:
return cursor;
}
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (sUriMatcher.match(uri)) {
case BOOK_DIT:
return "vnd.android.cursor.dir/vnd.com.example.h.content_demo_provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.h.content_demo_provider.book";
case CATEGORT_ITEM:
return "vnd.android.cursor.item/vnd.com.example.h.content_demo_provider.category";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.h.content_demo_provider.category";
default:
return null;
}
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Uri uriReturn = null;
switch (sUriMatcher.match(uri)) {
case BOOK_DIT:
case BOOK_ITEM:
long value = db.insert("book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + value);
return uriReturn;
//返回新插入行的行id,如果发生错误则返回-1
case CATEGORT_ITEM:
case CATEGORY_DIR:
long value2 = db.insert("category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + value2);
return uriReturn;
default:
return uriReturn;
}
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[]
selectionArgs) {
int deleteRows = 0;
switch (sUriMatcher.match(uri)) {
case BOOK_DIT:
deleteRows = db.delete("book", selection, selectionArgs);
return deleteRows;
case BOOK_ITEM:
String itemId1 = uri.getPathSegments().get(1);
deleteRows = db.delete("book", "id=?", new String[]{itemId1});
return deleteRows;
case CATEGORT_ITEM:
String itemId2 = uri.getPathSegments().get(1);
deleteRows = db.delete("category", "id=?", new String[]{itemId2});
return deleteRows;
case CATEGORY_DIR:
deleteRows = db.delete("category", selection, selectionArgs);
return deleteRows;
default:
return deleteRows;
}
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String
selection, @Nullable String[] selectionArgs) {
int updateRows = 0;
switch (sUriMatcher.match(uri)) {
case BOOK_DIT:
updateRows = db.update("book", values, selection, selectionArgs);
return updateRows;
case BOOK_ITEM:
String itemId1 = uri.getPathSegments().get(1);
updateRows = db.update("book", values, "id=?", new String[]{itemId1});
return updateRows;
case CATEGORT_ITEM:
String itemId2 = uri.getPathSegments().get(1);
updateRows = db.update("category", values, "id=?", new String[]{itemId2});
return updateRows;
case CATEGORY_DIR:
updateRows = db.update("category", values, selection, selectionArgs);
return updateRows;
default:
return updateRows;
}
}
}
关于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对应的MIME类型可以写成:
vnd.android.cursor.dir/vnd.content://com.example.app.provider/table1