权限以及ContentProvider学习
文章目录
1.运行时权限

权限参照表,如果是表中的权限,需要进行运行时权限处理,如果不在这张表只需要在AndroidManifest中添加一下声明权限就可以了
1.1在程序运行时申请权限
新建一个项目,在这个项目的基础上来学习运行时权限的使用方法,以CALL_PHONE权限作为示例
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/make_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打电话" />
</LinearLayout>
布局定义一个按钮,在点击按钮时就会触发拨打电话的逻辑,接着修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
Button makeCall = findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
可以看到,在按钮的点击事件中,构建了一个隐士Intent,Intent的action指定为Intent.CALL_PHONE,这是一个系统内置的打电话动作,在data部分制定了协议是tel,号码是10086。
接下来修改AndroidMainfest中声明如下权限
<uses-permission android:name="android.permission.CALL_PHONE" />
开始调试并且点击按钮发现什么都没有发生,

看到错误提示发现是Permission Denial,可以看出是由于去哪线被禁止所导致的,因为6.0以上的系统在实用为先权限是都必须要进行运行时权限处理。
修改主活动的代码
package com.example.runtimepermissiontest;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
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, String[] permissions, 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()方法判断用户是否已授权。该方法接收Context和具体权限名(如Manifest.permission.CALL_PHONE),返回值与PackageManager.PERMISSION_GRANTED比较,相等则已授权,不等则未授权。 - 已授权处理:若已授权,直接执行对应危险操作逻辑,文中将拨打电话逻辑封装在
call()方法。 - 未授权处理:未授权时,调用
ActivityCompat.requestPermissions()向用户申请授权。该方法需传入Activity实例、含权限名的String数组、唯一请求码(文中为 1 ),调用后系统弹出权限申请对话框。 - 授权结果回调:用户处理权限申请(同意或拒绝 )后,系统回调
onRequestPermissionsResult()方法,授权结果封装在grantResults参数。需判断结果,同意则调用call()执行操作,拒绝则放弃操作并弹出失败提示(如文中提示 “You denied the permission” )。 - 实际运行示例:重新运行程序点击
Make Call按钮,因未授权首次会弹出权限申请对话框,若用户点击DENY拒绝,会执行对应拒绝后的提示逻辑 。
发现系统会弹窗申请授权,授权成功就可以正常拨打电话了
2.内容提供器
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。那么接下来我们就一个一个开始学习吧,首先从使用现有的内容提供器开始。
如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android 系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面我们就来看一看,内容提供器到底是如何使用的。
2.1ContentResolver的基本用法
内容提供器数据访问关键类与操作
- 访问工具:应用程序访问内容提供器共享数据,需借助
ContentResolver类,可通过Context的getContentResolver()方法获取其实例 。 - CRUD 操作方法:
ContentResolver提供insert()(添加数据 )、update()(更新数据 )、delete()(删除数据 )、query()(查询数据 )方法,和SQLiteDatabase的 CRUD 方法功能对应,但参数有区别 。
内容 URI 相关
- 作用与组成:
ContentResolver增删改查方法不用表名参数,而用Uri参数(即内容 URI ),为内容提供器数据建立唯一标识符,由authority(区分不同应用,常以程序包名命名,如com.example.app.provider)和path(区分同一应用不同表,如/table1、/table2)两部分组成 。 - 标准格式:内容 URI 需在组合
authority和path后,头部加协议声明content://,如content://com.example.app.provider/table1,能清晰表达要访问哪个程序哪张表的数据 ,这也是ContentResolver方法接收Uri而非表名参数的原因(表名无法体现所属应用 )。
URI可以非常清楚的表达想要访问那个程序的那张表的数据,因此ContentResolver的CURD都是用Uri对象来作为参数,如果使用表名的话系统将无法得知我们期望访问的是那个应用程序里面的表
得到内容URI字符串,需要将他解析成Uri对象才可以作为参数传入,解析方法:
调用Uri.parse(),就可将URI字符串解析Uri对象了
一、查询数据(query 方法 )
-
方法调用:通过
getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder)查询内容提供器中数据,参数与 SQLite的query类似但更简洁,各参数对应 SQL 语句部分及作用:
query方法参数对应 SQL 部分 描述 urifrom table_name指定查询某应用程序下某张表 projectionselect column1, column2指定查询列名 selectionwhere column = value指定 where约束条件selectionArgs- 为 where占位符提供具体值sortOrderorder by column1, column2指定查询结果排序方式 -
结果处理:返回
Cursor对象,通过移动游标遍历数据,示例代码:
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
二、添加数据(insert 方法 )
- 操作步骤:创建
ContentValues组装数据,调用getContentResolver().insert(uri, values)插入,示例:
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
三、更新数据(update 方法 )
- 操作步骤:创建
ContentValues设更新值,调用getContentResolver().update(uri, values, selection, selectionArgs),用selection和selectionArgs约束更新范围,避免全表更新,示例:
ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", "1"});
这些操作围绕内容提供器的 CRUD(创建、读取、更新、删除 ),利用 ContentResolver 结合 Uri、ContentValues 等完成跨程序数据交互 。
2.2读取系统联系人
新建一个ContactsTest项目,希望读取出来的系统联系人可以在list中显示出来
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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: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="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
MainActivity
package com.example.contactstest;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.ContactsContract;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
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<>(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()) {
// 获取联系人姓名
@SuppressLint("Range") String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 获取联系人手机号
@SuppressLint("Range") 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, String[] permissions, 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:
}
}
}
主要看一下readContact方法,可以看到使用了ContentResolver的query方法来查询系统用的联系人数。传入的uri没有用.parse()去解析原因是ContactsContract.CommonDataKinds.Phone类已经帮我做好了封装提供了一个CONTENT_URI常量,这个常量就是.parse解析的,接着对Cursor对性进行遍历,将信息放到一个list之中。拼接并且在中间夹上换行符。最后通知ListView更新,别忘了关闭cursor。
,最后声明一下权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
这样就可以显示联系人了
小结:我们学习了如何在自己的程序中访问其他程序的数据。总的来说就是要获取到程序的内容URI,借助ContentResolver进行CURD。
2.3创建自己的内容提供器
- 内容提供器创建方式:实现跨程序共享数据,官方推荐用内容提供器,可通过新建类继承
ContentProvider来创建自定义内容提供器。 - 抽象方法重写要求:
ContentProvider类有 6 个抽象方法,子类继承时需全部重写,这 6 个方法分别是onCreate()、query()、insert()、update()、delete()、getType(),用于实现数据的创建、查询、插入、更新、删除以及获取数据类型等操作逻辑。
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;
}
}
一、ContentProvider 核心方法解析
- onCreate()
- 调用时机:初始化内容提供器时触发,仅当有
ContentResolver尝试访问程序数据时,内容提供器才会初始化。 - 功能:常在此完成数据库创建、升级等操作,返回
true表示初始化成功,false表示失败。
- 调用时机:初始化内容提供器时触发,仅当有
- query()
- 功能:从内容提供器查询数据。
- 参数作用:
uri:确定查询哪张表;projection:确定查询哪些列;selection+selectionArgs:约束查询哪些行(类似 SQL 的WHERE条件 );sortOrder:对查询结果排序。
- 返回:查询结果封装在
Cursor对象中返回。
- insert()
- 功能:向内容提供器添加一条数据。
- 参数作用:
uri确定添加到的表,values保存待添加数据。 - 返回:添加后,返回表示新记录的 URI。
- update()
- 功能:更新内容提供器中已有数据。
- 参数作用:
uri确定更新的表,values存新数据,selection+selectionArgs约束更新哪些行。 - 返回:受影响的行数作为返回值。
- delete()
- 功能:从内容提供器删除数据。
- 参数作用:
uri确定删除的表,selection+selectionArgs约束删除哪些行。 - 返回:被删除的行数作为返回值。
- getType()
- 功能:根据传入的内容 URI,返回对应的 MIME 类型。
二、内容 URI 解析与格式
- 核心作用:
ContentProvider方法多数带Uri参数,用于标识调用方期望访问的表和数据,需解析Uri以明确操作目标。 - 标准格式
- 访问表中所有数据:
content://com.example.app.provider/table1(路径结尾,对应com.example.app应用的table1表全量数据 )。 - 访问表中指定 ID 数据:
content://com.example.app.provider/table1/1(ID 结尾,对应com.example.app应用table1表中id=1的数据 )。
- 访问表中所有数据:
- 通配符匹配:
- 匹配
table1表任意一行数据的内容 URI 格式:content://com.example.app.provider/table1/#,借助这些通配符可灵活匹配不同需求的内容 URI,用于在内容提供器等场景中识别、处理数据访问请求 。 - 匹配任意表内容的URI:
content://com.example.app.provider/*
- 匹配
这些要点是理解 ContentProvider 工作机制、实现跨程序数据共享的基础,开发自定义内容提供器时,需围绕方法逻辑和 URI 解析做具体功能实现。
接着使用UriMatcher就饿可以匹配URI了。
UriMatcher作用与使用流程- 作用:用于轻松实现内容 URI 的匹配功能,帮助判断调用方期望访问的是哪张表的数据。
- 使用流程:
- 定义整型常量,分别标识不同的访问场景(如访问某表所有数据、某表单条数据 )。
- 在静态代码块中创建
UriMatcher实例,通过addURI()方法传入authority、path(可含通配符 )和自定义代码,将期望匹配的内容 URI 格式注册到UriMatcher中。 - 在
query()等ContentProvider方法中,调用UriMatcher的match()方法传入Uri对象,根据返回的自定义代码判断访问场景,进而执行对应的数据操作逻辑。
- 常量与匹配规则
- 定义 4 类整型常量(以
table1、table2为例 ):TABLE1_DIR(访问table1所有数据 )、TABLE1_ITEM(访问table1单条数据 )、TABLE2_DIR(访问table2所有数据 )、TABLE2_ITEM(访问table2单条数据 )。 addURI()方法中路径参数可使用通配符,如table2/#匹配table2表中带 id 的单条数据访问场景 。
- 定义 4 类整型常量(以
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;
// 静态代码块初始化 UriMatcher
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 注册期望匹配的内容 URI 格式及对应自定义代码
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);
uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);
uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// 通过 UriMatcher 匹配 Uri,根据返回代码判断访问场景
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;
}
return null;
}
// ContentProvider 其他抽象方法(onCreate、insert、update、delete、getType)需按实际逻辑重写,此处省略...
@Override
public boolean onCreate() {
return false;
}
@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;
}
}
getType()方法作用与 MIME 类型规则getType()是内容提供器必须实现的方法,用于获取Uri对应的 MIME 类型。- MIME 字符串组成规则:
- 必须以
vnd开头。 - 内容 URI 以路径结尾,后接
android.cursor.dir/;以id结尾,后接android.cursor.item/。 - 最后接
vnd..。
- 必须以
- 示例:
content://com.example.app.provider/table1(路径结尾 )→vnd.android.cursor.dir/vnd.com.example.app.provider.table1content://com.example.app.provider/table1/1(id结尾 )→vnd.android.cursor.item/vnd.com.example.app.provider.table1
MyProvider中getType()实现:通过UriMatcher匹配Uri,根据不同匹配结果(对应访问table1/table2的所有数据或单条数据场景 ),返回符合规则的 MIME 类型字符串。- 数据安全保障:内容提供器的 CRUD 操作需匹配内容 URI 格式,未向
UriMatcher添加隐私数据 URI,外部程序无法访问,保障数据安全。
public class MyProvider extends ContentProvider {
// 其他代码(如 UriMatcher 定义、query 等方法 )省略...
@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;
}
}

1111

被折叠的 条评论
为什么被折叠?



