Android入门(13)| Android权限 与 内容提供器

文章目录

  • 普通权限与危险权限
    • 运行时申请权限
  • 内容提供器
    • 运用安卓封装好的内容提供器
    • 自实现的内容提供器
      • 概念
      • 实现


普通权限与危险权限

主要用于不同应用程序之间在保证被访数据的安全性的基础上,实现数据共享的功能

在 Android 6.0 开始引入了运行时权限的功能,用户在安装软件时不需要一次性授权所有的权限,而是在软件的使用过程中再对某一项权限进行申请。Android 将权限分为两类:

  • 普通权限: 不会直接影响到用户的安全和隐私的权限,对于这部分权限,系统自动授权。
  • 危险权限: 可能会涉及到用户的隐私或者对设备安全性造成影响的权限。

危险权限如下,这些权限需要进行运行时权限处理,不在表中的权限只需要在 AndroidManifest.xml 添加权限声明即可:
Android入门(13)| Android权限 与 内容提供器_第1张图片
表中的每一个危险权限都属于一个权限组,虽然在进行权限处理的时候使用的是权限名,但是一旦用户同意授权,那么该权限名对应的权限组中的所有权限也会同时被授权

运行时申请权限

给按钮注册点击事件:

        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入门(13)| Android权限 与 内容提供器_第2张图片
这样的程序在 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() 方法检测用户是否已授权,该方法有两个参数:
    1. context
    2. 具体权限名
  • 未授权则需要调用 ActivityCompat.requestPermissions() 方法来向用户申请授权,该方法接受三个参数:
    1. Activity 实例,也就是当前活动。
    2. String 数组,也就是要申请的权限名。
    3. 请求码,必须唯一,这里传入 1。
  • 调用 requestPermissions 方法后,系统会弹出一个权限申请的对话框,用户可以选择同意或拒绝权限申请,不论同意与否,都会回调 onRequestPermissionsResult 方法,该方法有三个参数:
    1. 唯一的请求码
    2. 存储被申请权限名的 String 数组
    3. 授权结果 grantResults
    // 权限申请对话框点击结果回调
    @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入门(13)| Android权限 与 内容提供器_第3张图片
点击 DENY:
Android入门(13)| Android权限 与 内容提供器_第4张图片


内容提供器

内容提供器有两种:已有的(如 Android 系统自带的电话簿、短信等程序提供的供其他程序访问部分内部数据外部访问接口)、自实现的

ContentResolver类 是内容提供器的具体类,可以通过 Context类 中的 getContentResolver()方法 获取该类的实例,该类提供了一系列的 CRUD 操作,这些增删改查方法都使用 Uri参数 替代 表名参数内容URI 主要由三部分组成:

  • content: 协议声明;
  • authority: 用于区分不同应用程序,一般采用程序包名命名;
  • path: 用区分同一程序中不同表。

举个例子:在这里插入图片描述

内容URI 只是一串字符,还需通过 Uri.parse() 方法解析成 Uri对象 才可做为参数。

关于内容提供器的增删查改方法,这里仅解释较为复杂的 query() 方法:
Android入门(13)| Android权限 与 内容提供器_第5张图片
查询完后返回一个 Cursor对象,可以通过遍历其所有行来得到每一行数据。


运用安卓封装好的内容提供器

运用联系人应用的内容提供器,读取联系人信息并在 ListView 中显示。

声明权限:
Android入门(13)| Android权限 与 内容提供器_第6张图片
布局文件 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;
        }
    }
}

运行结果:
Android入门(13)| Android权限 与 内容提供器_第7张图片


自实现的内容提供器

概念

可以通过新建一个 ContentProvider子类 的方式来创建自己的内容提供器。ContentProvider类6抽象方法需要我们重写:onCreate()query()insert()update()delete()getType()。这里重点介绍 onCreategetType 两个方法:

  • onCreate():ContentProvider 尝试访问程序中数据时,初始化内容提供器,通常在这里完成对数据库的创建和升级等操作。返回 true 表内容提供器初始化成功,false 表失败。
  • getType(): 根据传入的 内容URI 来返回相应的 MIME 类型。MIME字符串 主要由三部分组成:
    1. 必须要以 vnd 开头
    2. 如果 内容URI路径 结尾,则后接 android.cursor.dir/,如果以 id 结尾,则后接 android.cursor.item/
    3. 最后接上 vnd..

内容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/tablevnd.android.cursor.dir/vnd.com.example.app.provider.table
  • content://com.example.app.provider/table/1vnd.android.cursor.item/vnd.com.example.app.provider.table

如何匹配 内容URI 呢?

首先借助 UriMatcher.addURI() 方法,将 内容URI的相关信息 添加进匹配器中,相关信息对应方法的三个参数:authoritypath(int)code。前两者之前讲过这里不再赘述,code 用以唯一标识要访问的资源

再借助 UriMatcher.match() 方法,传入一个 Uri对象 ,通过返回的 code 来匹配对应的操作。

如何保证隐私数据不泄露?

因为所有的 CRUD操作 都需要匹配到相应的 内容URI 格式才能进行,只要不向 UriMatcher 中添加 隐私数据的URI 就好。


实现

那现在开始自实现内容提供器,操作的数据库是该篇博客中的例子:

AndroidManifest.xml 文件中注册:
Android入门(13)| Android权限 与 内容提供器_第8张图片
自定义的内容提供器 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()

  1. 初始化一个 MyDatabaseHelper 实例;
  2. 返回 true 表示内容提供器初始化成功。

query()

  1. 通过 MyDatabaseHelper 获取 SQLiteDatabase 实例;
  2. 通过 uriMatcher.match(uri) 分析用户想访问的表;
  3. 通过 SQLiteDatabase.query() 进行查询,并返回 Cursor 对象:
    • 访问单条数据时,调用 uri.getPathSegments()内容URI 权限之后的部分以 “/” 作为分割,并将结果放入一个字符串列表,列表的第0个位置是路径,第1个位置则是id

insert()

  • 前两步同 query()
  1. 通过 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();
            }
        });
    }
}

如果模拟器是 Android 11,那么该程序的清单文件需要加上 标签,原因见本博客:
Android入门(13)| Android权限 与 内容提供器_第9张图片

你可能感兴趣的