文章目录
- 普通权限与危险权限
- 运行时申请权限
- 内容提供器
- 运用安卓封装好的内容提供器
- 自实现的内容提供器
- 概念
- 实现
普通权限与危险权限
主要用于不同应用程序之间在保证被访数据的安全性的基础上,实现数据共享的功能。
在 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()
方法检测用户是否已授权,该方法有两个参数:- context
- 具体权限名
- 未授权则需要调用
ActivityCompat.requestPermissions()
方法来向用户申请授权,该方法接受三个参数:- Activity 实例,也就是当前活动。
- String 数组,也就是要申请的权限名。
- 请求码,必须唯一,这里传入 1。
- 调用
requestPermissions
方法后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝权限申请,不论同意与否,都会回调onRequestPermissionsResult
方法,该方法有三个参数:- 唯一的请求码
- 存储被申请权限名的 String 数组
- 授权结果 grantResults
// 权限申请对话框点击结果回调@Overridepublic 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
。
- 第一次申请授权被拒绝,点击按钮仍会二次调用
- 已授权则直接调用
点击按钮的运行结果:
点击 DENY:
内容提供器
内容提供器有两种:已有的(如 Android 系统自带的电话簿、短信等程序提供的供其他程序访问部分内部数据的外部访问接口)、自实现的。
ContentResolver类 是内容提供器的具体类,可以通过 Context类 中的 getContentResolver()方法
获取该类的实例,该类提供了一系列的 CRUD 操作,这些增删改查方法都使用 Uri参数 替代 表名参数。内容URI 主要由三部分组成:
- content: 协议声明;
- authority: 用于区分不同应用程序,一般采用程序包名命名;
- path: 用区分同一程序中不同表。
举个例子:
内容URI 只是一串字符,还需通过 Uri.parse()
方法解析成 Uri对象 才可做为参数。
关于内容提供器的增删查改方法,这里仅解释较为复杂的 query()
方法:
查询完后返回一个 Cursor对象,可以通过遍历其所有行来得到每一行数据。
运用安卓封装好的内容提供器
运用联系人应用的内容提供器,读取联系人信息并在 ListView 中显示。
声明权限:
布局文件 contacts_layout.xml:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ListViewandroid: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<>();@Overrideprotected 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);}// 刷新ListViewadapter.notifyDataSetChanged();// 关闭 Cursor 对象cursor.close();}} catch (Exception e) {e.printStackTrace();} finally {// 和上面的关闭二选一/*if(cursor != null){cursor.close();}*/}}@Overridepublic 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
两个方法:
- onCreate(): 当 ContentProvider 尝试访问程序中数据时,初始化内容提供器,通常在这里完成对数据库的创建和升级等操作。返回
true
表内容提供器初始化成功,false
表失败。 - getType(): 根据传入的 内容URI 来返回相应的 MIME 类型。MIME字符串 主要由三部分组成:
- 必须要以
vnd
开头 - 如果 内容URI 以 路径 结尾,则后接
android.cursor.dir/
,如果以 id 结尾,则后接android.cursor.item/
- 最后接上
vnd.<authority>.<path>
- 必须要以
内容URI 的格式主要有两种:
- 以路径结尾表示期望访问表中所有数据:
content://com.example.app.provider/table
(访问 table 表中所有数据) - 以 id 结尾表示期望访问表中拥有相应 id 的数据:
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);}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic boolean onCreate() {dbHelper = new MyDatabaseHelper(getContext(), "Student.db", null, 4);return true;}@Overridepublic 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;}@Overridepublic 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()
- 初始化一个 MyDatabaseHelper 实例;
- 返回
true
表示内容提供器初始化成功。
query()
- 通过 MyDatabaseHelper 获取 SQLiteDatabase 实例;
- 通过
uriMatcher.match(uri)
分析用户想访问的表; - 通过
SQLiteDatabase.query()
进行查询,并返回 Cursor 对象:- 访问单条数据时,调用
uri.getPathSegments()
将 内容URI 权限之后的部分以 “/” 作为分割,并将结果放入一个字符串列表,列表的第0个位置是路径,第1个位置则是id。
- 访问单条数据时,调用
insert()
- 前两步同 query()
- 通过
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/";@Overrideprotected 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();}});}
}
如果模拟器是 Android 11,那么该程序的清单文件需要加上 <queries>
标签,原因见本博客: