这里写目录标题
7.1 内容提供器简介
内容提供器主要用于在不同的应用程序之间实现数据共享的功能。它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保是地方数据的安全性。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,保证程序中的隐私数据不会有泄露的风险。
7.2 运行时权限
为了更好的保护用户的安全和隐私,安卓系统在6.0中引入了运行时权限这个功能。
7.2.1 Android权限机制详解
第五章BroadcastTest中,为了访问系统的网络状态和监听开机广播,于是在AndroidManifest.xml添加了这样两句权限声明:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
如果不声明权限,系统运行就会崩溃。
用户不需要再安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中在对某一项权限申请进行授权。比如说相机应用在运行时申请了地理位置,因为就算我拒绝了这个权限,但是我仍然可以使用相机的其他功能。而不是像之前那样直接无法安装这个软件。
当然不是所有的权限都需要在运行时申请。对于用户来说不停的授权也很繁琐。安卓将所有的权限归成了两类,一类是普通权限,一类是危险权限。普通权限是,对于这部分权限的申请,系统会自动进行授权。
Android中的危险权限有九种。以下是九种权限组名。
- CALENDAR
- CAMERA
- CONTACTS
- LOCATION
- MICROPHONE
- PHONE
- SENSORS
- SMS
- STORAGE
7.2.2 在程序运行时申请权限
CALL_PHONE这个 权限是在编写拨打电话功能时需要声明的。因为拨打电话涉及到用户付费的问题,因此被列为了危险权限。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Button makeCall = (Button) findViewById(R.id.buttonmy);
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"));
}catch(SecurityException e) {
e.printStackTrace();
}
}
public void onRequestPermissionResult(int requestCode,String [] permissions,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:
}
}
}
7.3 访问其他程序中的数据
内容提供器的用法一般有两种,一种是从现有的内容提供器来读取和操作相应程序中的数据。另一种是创建自己的内容提供器,给程序的数据提供外部访问接口。如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问,安卓系统中自带的电话簿,短信,媒体库等程序都提供了类似的访问接口。使得三第三方应用程序可以充分的利用这部分数据。来实现更好的功能。
7.3.1 ContentResovler的用法
对于每一个应用程序来说,如果想要访问内容提供器中的共享数数据,一定要借助ContentResolver类,可以通过getContentResolver()方法获取到该类的实例。ContentResolver类中提供了一系列的方法对数据进行CRUD操作,其中insert的方法用于添加数据,Update用于更新数据,delete用于删除数据,query方法用于查询数据,有没有似曾相识的感觉?没错。SQLiteDatabase中也是用这几个方法来进行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为两个
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对象来查询table1表中的数据了
Cursor cursor=getContentResolver().query{
uri,
projection,
selection,
selectionArgs,
sortOrder);
query() 方法参数 对应SQL部分 描述
uri from table_name 指定查询某个应用程序下的某一张表
查询之后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一个行中相应列的数据
if(cursor!=null){
while(cursor.moveToNext()){
String column1=cursor.getSring(cursor.getColumnIndex("column1"));
int column2=cursor.getInt(cursor.getColumnIndex("column2"));
ContentValues Values=new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
getContentResolver().updata(uri,values,"column1=? and column2=?",new String[] {"text","1"});
getContentResolver().delete(uri,"column2=?" ,new String[] {"1"});
}
cursor.close();
}
7.3.2 读取系统联系人
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/contacts_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<
mainActivity.java
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
ListView contactView=(ListView) findViewById(R.id.contacts_view);
adapter =new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,contactsList);
contactView.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"+nunmber);
}
}
adapter.notifyDataSetChanged();
}
catch (Exception e){
e.printStackTrace();
}
finally {
if(cursor!=null){
cursor.close();
}
}
}
public void onRequestPermissionResult(){
}
}
Manifest.xml文件
<?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.ContactsTest"
tools:targetApi="31">
<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>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
</manifest>
7.4 创建自己的内容提供器
7.4.1 创建内容提供器的步骤
新建一个类继承ContentProvider,有6个抽象方法,在使用子类继承的时候,需要6个方法全部重写。
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
//数据库创建和升级
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
//内容URI返回相应的类型
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
一个标准的内容URI写法是这样的:
- content://com.example.app.provider/table1
在这个内容URI后面加一个id
- content://com.example.app.provider/table1/1
表示com.example.app.provider这个应用的table1表中id为1的数据
内容URI就两种格式,已路径结束表示期望访问表中所有数据;以id结尾期望访问表中相应id的数据。
可以用通配符方式匹配这两种格式的URI,规则如下:
- *:表示匹配任意长度的任意字符。
- #:表示匹配任意长度的数字。
所以,一个能够匹配任意表的内容URI格式可以写成:
content://com.example.app.provider/*
一个能够匹配table1表中任意一行数据的内容URI格式可以为
content://com.example.app.provider/table1/#
借助UriMatcher这个类就可以匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收了一个addURI方法,这个方法接收三个参数,可以分别把authority、path和一个自定义代码传进入。这样,当调用UriMatcher的match方法时,就可以将一个Uri对象传入,返回值是某个能否匹配这个Uri对象所对应的自定义代码,利用这个代码,就可以判断出调用方期望访问的是哪一个表中的数据了。
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;
public static UriMatcher uriMatcher;
static {
uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.app.provider","table1",TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);
}
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
}
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}