主要用于不同应用程序之间在保证被访数据的安全性的基础上,实现数据共享的功能。
在 Android 6.0 开始引入了运行时权限的功能,用户在安装软件时不需要一次性授权所有的权限,而是在软件的使用过程中再对某一项权限进行申请。Android 将权限分为两类:
危险权限如下,这些权限需要进行运行时权限处理,不在表中的权限只需要在 AndroidManifest.xml 添加权限声明即可:
表中的每一个危险权限都属于一个权限组,虽然在进行权限处理的时候使用的是权限名,但是一旦用户同意授权,那么该权限名对应的权限组中的所有权限也会同时被授权。
给按钮注册点击事件:
Button button1 = findViewById(R.id.button_1);
button1.setOnClickListener((View view)->{
try {
/*// 打开拨号界面,无需声明权限
Intent intent = new Intent(Intent.ACTION_DIAL);*/
// 打电话,需要生命权限
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:15309276440"));
startActivity(intent);
} catch (SecurityException e){
e.printStackTrace();
}
});
在注册表中加入:
这样的程序在 Android 6.0 之前都可以正常运行,但是在更高的版本点击按钮后没有任何效果,错误信息如下:
权限被禁止。
修复这个问题,申请运行时权限的流程:
将打电话的行为封装成函数 call()
:
private void call(){
try {
/*// 打开拨号界面,无需声明权限
Intent intent = new Intent(Intent.ACTION_DIAL);*/
// 打电话,需要声明权限
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:15309276440"));
startActivity(intent);
} catch (SecurityException e){
e.printStackTrace();
}
}
修改 onCreate
方法内的点击按钮行为:
Button button1 = findViewById(R.id.button_1);
button1.setOnClickListener((View view)->{
// 相等说明用户已授权,不等说明未授权
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED){
// 申请授权
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.CALL_PHONE}, 1);
} else {
call();
}
});
ContextCompat.checkSelfPermission()
方法检测用户是否已授权,该方法有两个参数:
ActivityCompat.requestPermissions()
方法来向用户申请授权,该方法接受三个参数:
requestPermissions
方法后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝权限申请,不论同意与否,都会回调 onRequestPermissionsResult
方法,该方法有三个参数:
// 权限申请对话框点击结果回调
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "用户拒绝授权", Toast.LENGTH_LONG).show();
}
break;
default:
}
if(!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CALL_PHONE)){
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle("电话权限不可用")
.setMessage("请在-应用设置-权限中,允许APP使用电话权限。");
dialog.setCancelable(false);
dialog.setPositiveButton("立即设置", (dialog1, which) -> goToAppSetting());
dialog.setNegativeButton("取消", (dialog2, which) -> dialog2.dismiss());
dialog.show();
}
}
shouldShowRequestPermissionRationale
方法的返回值:
true
true
false
false
总结:该方法返回值表示需不需要向用户解释一下你的 app 为什么需要这个权限。当用户已经授权或者用户明确禁止(权限被禁用且不再提示)的时候就不需要再去解释了,所以此时会返回 false
。
权限不可用时引导用户手动启用权限:
// 跳转到权限设置界面
private void goToAppSetting() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
上述代码的运行逻辑是:
checkSelfPermission
检验用户是否已授权:
call
打电话;requestPermissions
申请授权:
requestPermissions
,此时 shouldShowRequestPermissionRationale
返回值为 true
;requestPermissions
不会再弹出询问弹窗,但是仍会回调 onRequestPermissionsResult
,此时 shouldShowRequestPermissionRationale
返回值为 false
,因此会弹出对话框询问用户是否要跳转到设置界面开启权限,用户可以通过 “立即设置” 跳转到 setting界面 来开放权限,此后再点击按钮会因为已授权而不再调用 requestPermissions
。内容提供器有两种:已有的(如 Android 系统自带的电话簿、短信等程序提供的供其他程序访问部分内部数据的外部访问接口)、自实现的。
ContentResolver类 是内容提供器的具体类,可以通过 Context类 中的 getContentResolver()方法
获取该类的实例,该类提供了一系列的 CRUD 操作,这些增删改查方法都使用 Uri参数 替代 表名参数。内容URI 主要由三部分组成:
内容URI 只是一串字符,还需通过 Uri.parse()
方法解析成 Uri对象 才可做为参数。
关于内容提供器的增删查改方法,这里仅解释较为复杂的 query()
方法:
查询完后返回一个 Cursor对象,可以通过遍历其所有行来得到每一行数据。
运用联系人应用的内容提供器,读取联系人信息并在 ListView 中显示。
声明权限:
布局文件 contacts_layout.xml:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
活动文件 ContactsActivity:
public class ContactsActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contacts_layout);
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(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 {
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
cursor = getContentResolver().query(uri, null, null,
null, null, null);
if(cursor != null){
while(cursor.moveToNext()){
// 获取联系人姓名
String name = cursor.getString(cursor.getColumnIndex(ContactsContract
.CommonDataKinds.Phone.DISPLAY_NAME));
// 获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex(ContactsContract
.CommonDataKinds.Phone.NUMBER));
contactsList.add(name + "\n" + number);
}
// 刷新ListView
adapter.notifyDataSetChanged();
// 关闭 Cursor 对象
cursor.close();
}
} 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, "用户拒绝授权", Toast.LENGTH_LONG).show();
}
break;
}
}
}
可以通过新建一个 ContentProvider子类 的方式来创建自己的内容提供器。ContentProvider类 有 6
个抽象方法需要我们重写:onCreate()
、query()
、insert()
、update()
、delete()
、getType()
。这里重点介绍 onCreate
和 getType
两个方法:
true
表内容提供器初始化成功,false
表失败。vnd
开头android.cursor.dir/
,如果以 id 结尾,则后接 android.cursor.item/
vnd..
内容URI 的格式主要有两种:
content://com.example.app.provider/table
(访问 table 表中所有数据)content://com.example.app.provider/table/1
(访问 table 表中 id 为 1 的数据)还可以使用通配符:
content://com.example.app.provider/*
table
表中任意一行数据:content://com.example.app.provider/table/#
内容URI 对应的 MIME类型:
content://com.example.app.provider/table
: vnd.android.cursor.dir/vnd.com.example.app.provider.table
content://com.example.app.provider/table/1
:vnd.android.cursor.item/vnd.com.example.app.provider.table
如何匹配 内容URI 呢?
首先借助 UriMatcher.addURI()
方法,将 内容URI的相关信息 添加进匹配器中,相关信息对应方法的三个参数:authority
、path
、(int)code
。前两者之前讲过这里不再赘述,code
用以唯一标识要访问的资源。
再借助 UriMatcher.match()
方法,传入一个 Uri对象 ,通过返回的 code
来匹配对应的操作。
如何保证隐私数据不泄露?
因为所有的 CRUD操作 都需要匹配到相应的 内容URI 格式才能进行,只要不向 UriMatcher 中添加 隐私数据的URI 就好。
那现在开始自实现内容提供器,操作的数据库是该篇博客中的例子:
在 AndroidManifest.xml
文件中注册:
自定义的内容提供器 MyContentProvider
:
public class MyContentProvider extends ContentProvider {
public static final int STUDENT_DIR = 0;
public static final int STUDENT_ITEM = 1;
public static final int CLASS_DIR = 2;
public static final int CLASS_ITEM = 3;
public static final String AUTHORITY = "com.example.activitytest.CustomType.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "student", STUDENT_DIR);
uriMatcher.addURI(AUTHORITY, "student/#", STUDENT_ITEM);
uriMatcher.addURI(AUTHORITY, "class", CLASS_DIR);
uriMatcher.addURI(AUTHORITY, "class/#", CLASS_ITEM);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deleteRows = 0;
switch (uriMatcher.match(uri)){
case STUDENT_DIR:
deleteRows = db.delete("Student", selection, selectionArgs);
break;
case STUDENT_ITEM:
String studentId = uri.getPathSegments().get(1);
deleteRows = db.delete("Student", "id = ?", new String[]
{studentId});
break;
case CLASS_DIR:
deleteRows = db.delete("Class", selection, selectionArgs);
break;
case CLASS_ITEM:
String classId = uri.getPathSegments().get(1);
deleteRows = db.delete("Class","id = ?", new String[]
{classId});
break;
}
return deleteRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case STUDENT_DIR:
return "vnd.android.cursor.dir/vnd.com.example.activitytest.CustomType.provider.student";
case STUDENT_ITEM:
return "vnd.android.cursor.item/vnd.com.example.activitytest.CustomType.provider.student";
case CLASS_DIR:
return "vnd.android.cursor.dir/vnd.com.example.activitytest.CustomType.provider.class";
case CLASS_ITEM:
return "vnd.android.cursor.item/vnd.com.example.activitytest.CustomType.provider.class";
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)){
case STUDENT_DIR:
case STUDENT_ITEM:
long studentId = db.insert("Student", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/student/" + studentId);
break;
case CLASS_DIR:
case CLASS_ITEM:
long classId = db.insert("Class", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/class/" + classId);
break;
default:
break;
}
return uriReturn;
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "Student.db", null, 4);
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 STUDENT_DIR:
cursor = db.query("Student", projection, selection, selectionArgs,
null, null, sortOrder);
break;
case STUDENT_ITEM:
// Uri字符串中以 “/” 作为分割,0部分是路径,1部分则是id。即获取Uri字符串中的id部分。
String studentId = uri.getPathSegments().get(1);
cursor = db.query("Student", projection, "id = ?", new String[]
{ studentId }, null, null, sortOrder);
break;
case CLASS_DIR:
cursor = db.query("Class", projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CLASS_ITEM:
String classId = uri.getPathSegments().get(1);
cursor = db.query("Class", projection, "id = ?", new String[]
{ classId }, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updateRows = 0;
switch (uriMatcher.match(uri)){
case STUDENT_DIR:
updateRows = db.update("Student", values, selection, selectionArgs);
break;
case STUDENT_ITEM:
String studentId = uri.getPathSegments().get(1);
updateRows = db.update("Student", values, "id = ?", new String[]
{studentId});
break;
case CLASS_DIR:
updateRows = db.update("Class", values, selection, selectionArgs);
break;
case CLASS_ITEM:
String classId = uri.getPathSegments().get(1);
updateRows = db.update("Class", values, "id = ?", new String[]
{classId});
break;
default:
break;
}
return updateRows;
}
}
onCreate()
true
表示内容提供器初始化成功。query()
uriMatcher.match(uri)
分析用户想访问的表;SQLiteDatabase.query()
进行查询,并返回 Cursor 对象:
uri.getPathSegments()
将 内容URI 权限之后的部分以 “/” 作为分割,并将结果放入一个字符串列表,列表的第0个位置是路径,第1个位置则是id。insert()
SQLiteDatabase.insert()
进行添加,但由于该方法要求返回一个 Uri对象,因此需要调用 Uri.parse()
将 URI字符串 解析成 Uri对象。接下来新建一个程序,用来调用上面的内容提供器:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private String newId;
public static final String AUTHORITY = "content://com.example.activitytest.CustomType.provider/";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button_add = findViewById(R.id.button_add);
button_add.setOnClickListener(v->{
Uri uri = Uri.parse(AUTHORITY + "student/");
ContentValues values = new ContentValues();
values.put("name", "zj");
values.put("age", 21);
values.put("weight", 90);
values.put("gender", "girl");
Uri insertUri = getContentResolver().insert(uri, values);
newId = insertUri.getPathSegments().get(1);
Log.e(TAG, "咕咕:"+insertUri.toString());
});
Button button_query = findViewById(R.id.button_query);
button_query.setOnClickListener(v->{
Uri uri = Uri.parse(AUTHORITY + "student");
Cursor cursor = getContentResolver().query(uri, null, null,
null, null);
while(cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex("name"));
int age = cursor.getInt(cursor.getColumnIndex("age"));
double weight = cursor.getDouble(cursor.getColumnIndex("weight"));
String gender = cursor.getString(cursor.getColumnIndex("gender"));
String res = name + " " + age + " " + weight + " " + gender;
Toast.makeText(this, res, Toast.LENGTH_LONG).show();
}
cursor.close();
Log.e(TAG, "表中数据显示完毕");
});
Button button_update = findViewById(R.id.button_update);
button_update.setOnClickListener(v->{
Uri uri = Uri.parse(AUTHORITY + "student/" + newId);
ContentValues values = new ContentValues();
values.put("name", "cjl");
values.put("weight", 95);
getContentResolver().update(uri, values, null, null);
});
Button button_delete = findViewById(R.id.button_delete);
button_delete.setOnClickListener(v->{
if(newId!=null && newId.compareTo("0") > 0){
Uri uri = Uri.parse(AUTHORITY + "student/" + newId);
getContentResolver().delete(uri, null, null);
newId = String.valueOf(Integer.valueOf(newId)-1);
Log.e(TAG, "最后一个id:" + newId);
}
else{
Toast.makeText(this, "表中已经没有数据了", Toast.LENGTH_LONG).show();
}
});
}
}