四大组件——内容提供器(Content Provider)详解
@author 无忧少年
@createTime 2021/08/15
1.内容提供器简介
? 内容提供器(Content Provider)主要用于再不同的应用程序之间实现数据共享的功能,他提供了一台完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问的数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。
? 不同于文件存储和SharedPreferences存储中的两种全局可读写的操作模式,内容提供器可以选择对哪一部分数据进行共享,也就是对权限进行控制,从而保证我们应用程序中的隐私数据不会有泄露的风险。
? 再介绍内容提供器之前,需要先了解一下Android运行时权限,因为一会的内容提供器会使用到运行时权限,当然不止内容提供器会使用,当我们进行开发工作的时候会经常和权限打交道。
2.运行时权限
? Android的权限机制并不是什么新鲜事物,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制再保护用户安全和隐私等方面渠道的作用比较有限,尤其是一些大家都离不开的常用软件,非常容易“店大欺客”。为此,Adnroid开发团队再Android 6.0系统中inrush了运行时权限这个功能,从而更好的保护了用户的安全和隐私。
2.1 Android权限机制详解
? 在之前的广播机制(Roadcast)中有申请过系统的网络状态的权限,保证能监听到网络变化的广播,需要再AndroidManifest.xml中加入以下声明
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.broadcasttest">
...
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
...
</manifest>
? 因为访问系统的网络状态涉及了用户设备的安全险,因此必须要在AndroidManifest.xml加入权限声明,否则程序就会因无权限崩溃。
? 权限主要分为两大类:普通权限和危险权限,再之前广播机制(Roadcast)中有申请过系统的网络状态的权限就是一种普通权限,这种权限值得是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限的申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了。危险权限则是表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。
? 具体所有的权限可以看官方文档https://developer.android.google.cn/reference/android/Manifest.permission
2.2 程序运行时申请权限
? 首先先新建一个RuntimePermissonTest的项目,就再这个项目上来学习运行时权限相关的使用方法。主要就使用CALL_PHONE这个权限,需要再页面加个按钮,然后加上监听,点击按钮就拨打10010电话,同时也需要在AndroidManifest.xml加入权限声明,代码如下:
activity_main.xml
<LinearLayout 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"
tools:context=".MainActivity">
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call">
</Button>
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.runtimepermissontest">
<permission android:name="android.permission.CALL_PHONE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Androidtest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.make_call);
button.setOnClickListener(view -> {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10010"));
startActivity(intent);
});
}
}
代码看起来还是比较简单,运行一下试试,点击了一下按钮,发现没任何效果并且程序闪退了,看一下控制台的日志如下:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.runtimepermissontest, PID: 1983
java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxx cmp=com.android.server.telecom/.components.UserCallActivity } from ProcessRecord{f209fb5 1983:com.example.runtimepermissontest/u0a404} (pid=1983, uid=10404) requires android.permission.CALL_PHONE
? 提示我们“Permission Denial”,可以看出,是由于权限被禁止所导致的,因为6.0及以上的系统在使用危险权限时都必须进行运行时的权限处理。接下来要修改一下MainActivity.java的代码,如下所示
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.make_call);
button.setOnClickListener(view -> {
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, 1);
} else {
call();
}
});
}
private void call() {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10010"));
startActivity(intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, 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()来判断是否有相应的权限,如果没有权限则调用ActivityCompat.requestPermissions()进行提醒用户进行授权,然后会回调onRequestPermissionsResult()方法将用户授权结果通知我们,我们判断一下当前用户的授权结果,判断是进行拨打电话还是进行弹出提示框。运行截图如下:
3.访问其他程序中的数据
? 内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问的接口。
3.1 ContentResolver的基本用法
? 对每一个应用程序来说,如果想要访问内容提供其中共享的数据,就一定借助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,然后吧authority和path进行组合,内容UIRI就变成了com.example.app.provider/table1、com.example.app.provider/table2。这两个URI头部在加上表示就可以被标识为内容的URI了,格式如下content://com.example.app.provider/table1、content://com.example.app.provider/table2
? 在得到了内容URI字符串之后使用Uri.parse()方法将字符串转化为URI对象,然后就可以使用这个uri对象进行查询数据了,代码如下
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
? 参数和SQLiteDatabase中的query()方法的参数大同小异,但是总体来说要简单一点,毕竟sql会比较简单。
? 返回了cursor实例,可以对cursor进行遍历取出查询出来的值,新增和修改就不举例子了,大同小异和SQLiteDatabase中的使用没什么太大区别。
3.2 读取系统联系人
? 新建一个ContactsTest项目,同样也要申请通信录的读取权限,代码如下:
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 = findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.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);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, 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:
}
}
}
activity_main.xml
<LinearLayout 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"
tools:context=".MainActivity">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contactstest">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Androidtest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
? 正题代码比较简单,和之前示例拨打电话只是多了数据初始化的部分,运行截图如下
4.创建自己的内容提供器
### 4.1 创建内容提供器步骤
? 创建还是熟悉的套路,点击com.example.broadcasttest右键新建->new->Other->Content Provider然后类为MyContentProvider 这样一个默认的内容提供器就创建完成了,当然当前内容提供器也是需要在AndroidManifest.xml进行注册的,只不过这一步ide已经给我们完成了,来看下代码
MyContentProvider.java
public class MyContentProvider extends ContentProvider {
String TAG="MyContentProvider";
public MyContentProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d(TAG,"insert delete");
return 0;
}
@Override
public String getType(Uri uri) {
Log.d(TAG,"insert getType");
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG,"insert insert");
return null;
}
@Override
public boolean onCreate() {
Log.d(TAG,"insert onCreate");
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d(TAG,"insert query");
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.d(TAG,"insert update");
return 0;
}
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contenttest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Androidtest">
<provider
android:name=".MyContentProvider"
android:authorities="com.example.contactstest"
android:enabled="true"
android:exported="true"></provider>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
? 在上边重写的六个方法汇总,大多数已经很熟悉了,来简单的介绍一下
-
onCreate():初始化内容提供器的时候调用。通常会在这里完成对数据的创建和升级等操作,返回true表示内容提供器初始化成功,返回false则表示失败。 -
query():从内容提供器中查询数据。使用uri参数来确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。 -
insert():向内容提供器中添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URL。 -
update():更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。 -
delete():从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪行,被删除的行将作为返回值返回。 -
getType():根据传入的内容URI来返回相应的MIME类型。
可以看到,几乎每个方法都携带了uri这个参数,这个参数正式调用ContentResolver的增删改查方法时传递过来的。而现在需要对传入的Uri参数进行解析,从中分析出调用方法期望访问的表和数据。
? 具体代码示例参考https://www.cnblogs.com/jiqing9006/p/7704125.html
|