Android基础回顾(六)| 关于 Content provider

参考书籍:《第一行代码》 第二版 郭霖
如有错漏,请批评指出!

内容提供器(Content Provider)主要用于在不同应用程序之间实现数据共享功能(即跨程序数据共享)。它提供了一套完整的机制,允许一个程序访问另一个程序的数据,同时还能保证被访问数据的安全性。目前,使用 Content Provider 是Android实现跨程序共享数据的标准方式。Content Provider可以选择只对某一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄露的风险。

由于后面关于Content Provider的内容会大量涉及运行时权限,我们先来了解一下关于Android运行时权限的内容。

Android运行时权限

  • 关于Android权限机制
    在Android开发过程中,我们会经常遇到某些功能需要设备的一些权限,比如读取联系人、开启摄像头等。在Android6.0之前,这个问题是很好解决的,只需要在AndroidManifest文件中声明对应权限即可,当用户安装程序时,这些权限都会一一列出来,用户继续安装就意味着同意应用所请求的所有权限,这也就导致了很多应用程序滥用权限的情况,造成了用户隐私的安全问题。

    Android基础回顾(六)| 关于 Content provider_第1张图片

    而在Android6.0之后的系统中,加入了运行时权限的功能。也就是说,用户不需要在安装程序时一次性授权所有权限,可以在使用过程中,对相应功能提出的授权申请选择授权或拒绝授权,比如说,我们在初次使用QQ的扫一扫功能时,QQ会申请相机权限:
    Android基础回顾(六)| 关于 Content provider_第2张图片

    如果我们允许,就意味着对这个权限授权了,后面不需要再请求,如果我们拒绝了,这个功能就无法使用,除非你在手机的权限管理中手动授予应用这个权限。

    当然,并不是所有的权限都需要动态申请,在加入运行时权限功能后,Android将权限分为两个大类,一类是普通权限(和Android6.0之前一样,系统会自动授权),另一类是危险权限,即需要我们动态申请的权限。下面是Android中所有的危险权限,一共9组24个权限:

    Android基础回顾(六)| 关于 Content provider_第3张图片

    也就是说,在Android6.0以上的系统使用上面这些权限时,我们不仅要在AndroidManifest文件中声明,还需要动态申请,不在这张表里的权限,我们只需要在AndroidManifest文件中声明一下即可。上面这些是关于Android权限机制的内容,接下来我们来看看如何动态申请权限。

  • 在程序运行时申请权限
    在上面的表中,PHONE组有一个 CALL_PHONE权限,这个权限是拨打电话需要的,我们现在就使用这个权限来写一个拨打10086的功能。

    1. 首先在界面中添加一个Button,用来触发拨打电话(很简单,不贴代码了)。
    2. 在AndroidManifest文件中声明权限.
    
    
    1. 在Activity中编写点击按钮触发拨打电话的逻辑
    public class CpMainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.cp_activity_main);
            ButterKnife.bind(this);
    
            initView();
        }
    
        private void initView() {
            Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
            setSupportActionBar(toolbar);
        }
    
        @OnClick(R.id.but_call)
        public void onClick(View v) {
            switch (v.getId()){
                default:
                    break;
                case R.id.but_call:
                    try {
                         Intent intent = new Intent(Intent.ACTION_CALL);
                         intent.setData(Uri.parse("tel:10086"));
                         startActivity(intent);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    break;
            }
        }
    }
    

    只需要看onClick()方法中的内容,逻辑很简单,就是当我们点击我们定义的Button时,定义一个隐式Intent,Intent的action指定为 “Intent.ACTION_CALL”,这是一个系统内置的打电话的动作,然后data部分指定协议为 tel ,号码是10086,也就是当我们点击Button时,会直接拨打10086,(写在try catch 语句块中是为了防止程序崩溃)。不过,这是在Android 6.0 之前的写法,如果手机的Android版本低于6.0,这么写是可以。但是在Android6.0以上的系统中,我们点击Button是没有反应的(可以自行测试一下)。下面我们来看在Android6.0以上的系统中要如何申请权限:

    @OnClick(R.id.but_call)
    public void onClick(View v) {
        switch (v.getId()){
            default:
                break;
            case R.id.but_call:
                if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
                        != PackageManager.PERMISSION_GRANTED){
                    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1);
                }else {
                    try {
                        Intent intent = new Intent(Intent.ACTION_CALL);
                        intent.setData(Uri.parse("tel:10086"));
                        startActivity(intent);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                break;
        }
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            default:
                break;
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    try {
                        Intent intent = new Intent(Intent.ACTION_CALL);
                        intent.setData(Uri.parse("tel:10086"));
                        startActivity(intent);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }else {
                    ToastUtil.showShortToast(this, "您已拒绝电话权限,请手动开启");
                }
                break;
        }
    }
    

    首先,在事件触发时,我们需要调用 ContextCompat 的 checkSelfPermission(Context context, String permission)方法来验证我们需要使用的权限是否授权,具体的权限我们使用 Manifest.permission 来获取,这个方法有一个返回值,如果和 PackageManager.PERMISSION_GRANTED 相等,说明用户已经授权,我们可以拨打电话,否则我们就需要进行申请。使用 ActivityCompat 的 requestPermissions(Activiti activity, String[] permissions, int requestCode) 方法,可以向用户申请权限,这个方法接收三个参数,第一个参数即当前Activity,第二个参数是一个String数组,也就是说可以同时申请多条权限,将权限名放入数组即可,第三个参数是一个请求码,用来区分我们申请权限的位置,只要是唯一值就行。

    在调用完requestPermission()方法后,系统会弹出一个申请权限的对话框,用户可以选择同意或者拒绝,无论用户选择什么,最后都会回调 onRequestPermissionsResult() 方法,这时我们需要再来检查用户是否授权,授权了就执行我们的逻辑(即拨打电话),否则,我们可以弹出一条Toast用来提示操作失败。下面来看看效果:

    关于动态权限的内容就到这里,下面来看关于Content Provider 的用法。Content Provider的用法一般有两种,一种是使用现有的Content Provider 来读取和操作相应程序中的数据,另一种是创建自己的内容提供器,给我们程序的数据提供外部访问接口。

访问其他程序中的数据

  • ContentResolver的基本用法
    一个应用程序想要访问其他程序通过Content Provider共享的数据,就需要用到Content Resolver 类,可以通过Context的getContentResolver() 方法获取到该类的实例,ContentResolver中提供了一系列方法用于对数据库进行CRUD操作,和SQLiteDatabase的CRUD操作相似。不过ContentResolver 的CRUD方法不接收表名参数,而是使用一个Uri参数代替,这个参数被称为内容URI
    内容URI给Content Provider 中的数据建立了唯一的标示符,它主要由两部分组成:authority和path。authority可以用包名来命名,用于区分应用程序;path用于区分程序中的表。例如某个程序的包名是com.laughter.learnapplication,并且数据库中有两张表table1和table2,那么内容URI就是:
    content://com.laughter.learnapplication/table1
    content://com.laughter.learnapplication/table2
    “ content:// " 是协议声明,需要加在头部。这样看来,内容URI可以清楚表达我们想要访问那个程序的哪张表,也可以准确标识我们想要访问的应用程序中的数据。
  1. 查询数据
    想要查询Content Provider中的数据,我们首先要调用 Uri 的 parse() 方法将内容URI解析成Uri对象,

    Uri uri = Uri.parse("content://com.laughter.learnapplication/table1");
    

    接下来就可以查询数据了:

    Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
    

    下表是查询语句中的参数说明:
    Android基础回顾(六)| 关于 Content provider_第4张图片

    查询完成后返回的是一个 Cursor 对象,我们需要从 Cursor 对象中将数据逐条读出:

    if(cursor != null){
        while(cursor.moveToNext()){
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String num = cursor.getInt(cursor.getColumnIndex("num"));
        }
        cursor.close();
    }
    
  2. 添加数据

    ContentValues values = new ContentValues();
    values.put("name", name);
    values.put("num", num);
    getContentResolver().insert(uri, values);
    

    添加数据和SQLiteDatabase一样,要借助于ContentValues;

  3. 修改数据
    假设要修改 name 为 laughter,num 为 0 这一条数据的 num 属性

    ContentValues values = new ContentValues();
    values.put("num", num);
    getContentResolver().update(uri, values, "name = ? and num = ?", new String[] { "laughter", "0" });
    

    我们使用了 selection 和 selectionArgs这两个参数对想要更新的数据进行约束,保证修改的是我们想要修改的数据。

  4. 删除数据

    getContentResolver().delete(uri, "name = ?", new String[] { "laughter" });
    

    操作和SQLiteDatabase 相似,很容易理解。

  • 读取系统联系人
    接下来根据上面的内容实现一个读取系统联系人并显示出来的小应用。
  1. 首先在Activity布局中添加一个ListView

    
    
  2. 查询联系人数据,并使用ListView显示出来,简单起见,我们使用系统提供的 ArrayAdapter 以及 android.R.layout.simple_list_item_1 布局

    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.list_view)
        ListView listView;
    
        private ArrayAdapter adapter;
        private List contctsList = new ArrayList<>();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
    
            initView();
        }
    
        private void initView() {
            
            adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, contctsList);
            listView.setAdapter(adapter);
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
                    != PackageManager.PERMISSION_GRANTED){
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 0);
            }else {
                quaryContacts();
            }
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode){
                case 0:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                        quaryContacts();
                    }else {
                        ToastUtil.showShortToast(this, "您已拒绝授权,无法读取您的手机联系人");
                    }
                    break;
                default:
                    break;
            }
        }
    
        private void quaryContacts() {
            Cursor cursor = null;
            try{
                cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                        null, null, null, null);
                if (cursor != null){
                    while (cursor.moveToNext()){
                        String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                        String tel = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                        contctsList.add("\n" + name + "\n\n" + tel + "\n");
                    }
                    adapter.notifyDataSetChanged();
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if (cursor != null){
                    cursor.close();
                }
            }
        }
    }
    

    关于ListView的使用在前面的文章 Android基础回顾(三)| 常用控件 — ListView和RecyclerView 中已经讲得很清楚了,ButterKnife 的使用也讲过 Android实践(二) | 注解框架ButterKnife基本使用 ,我们直接看代码的逻辑,首先初始化ListView,然后检查权限,这里使用的 READ_CONTACTS 权限属于危险权限,也是需要在AndroidMenifest文件中声明,并且动态申请的:

    
    

    用户授权后,就开始查询手机中的联系人,这里我们的Uri参数给的是 ContactsContract.CommonDataKinds.Phone.CONTENT_URI,这是Android开发团队封装好的,我们可以打印出来看看:

    就是这样的一个值,我们直接使用就行,因为我们要获取所有联系人,所以不需要加约束条件,后面的参数都传null就行了。接着遍历查询的结果 Cursor 对象,ContactsContract.CommonDataKinds.Phone.DISPLAY_NAMEContactsContract.CommonDataKinds.Phone.NUMBER 分别是联系人姓名和联系人号码对应的常量,直接使用,取出数据后逐条添加到ListView的数据源中,然后更新一下ListView,最后别忘了关闭Cursor对象。下面来运行一下看看效果吧:

    Android基础回顾(六)| 关于 Content provider_第5张图片


上一篇:Android基础回顾(五)| 数据存储——持久化技术
下一篇:Android基础回顾(七)| 使用手机多媒体


你可能感兴趣的:(Android基础回顾(六)| 关于 Content provider)