美文网首页安卓开发
Android四大组件之内容提供器(Content Provid

Android四大组件之内容提供器(Content Provid

作者: 天涯的尽头s风沙 | 来源:发表于2019-05-10 16:33 被阅读7次

    (内容来自《Android第一行代码(第二版)》)

    附:Android基础之四大组件

    本文目录

    1. 运行时权限

    • 1.1 Android权限机制详解
    • 1.2 在程序运行时申请权限

    2. 访问其他程序中的数据

    • 2.1 ContentResolver的基本用法
    • 2.2 读取系统联系人

    3. 创建自己的内容提供器

    • 3.1 创建内容提供器的步骤
    • 3.2 实现跨程序共享

    分割线


    内容提供器( Content Provider)主要用于在不同的应用程序之间实现数据共享
    它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。

    目前,使用内容提供器是 Android实现跨程序共享数据的标准方式。
    不同于文件存储和 SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

    不过在正式开始学习内容提供器之前,我们需要先掌握另外一个非常重要的知识——Android运行时权限

    1.运行时权限

    1.1Android权限机制详解

    Android权限机制从系统的第一个版本就已经开始存在了,但是在Android6.0系统中才加入运行时权限功能。

    在6.0版本之前,安装软件需要一次性授权所有申请的权限,如果不认可那么就拒绝安装,这样当一些软件让我们产生依赖时就容易店大欺客,,要么不用,要用就要同意软件所申请的权限,这样就存在着滥用权限的情况。

    在6.0版本之后,安卓开发团队在系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。

    比如说一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。

    当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很烦琐。

    Android现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。
    普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了。
    危险权限则表示那些可能会触及用户隐私,或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。

    但是Android中有一共有上百种权限,我们怎么从中区分哪些是普通权限,哪些是危险权限呢?
    其实危险权限总共就那么几个,除了危险权限之外剩余的就都是普通权限了。

    下表列出了Android中所有的危险权限,一共924个权限

    图片.png

    你并不需要了解表格中每个权限的作用,只要把它当成一个参照表来査看就行了。

    每当要使用一个权限时,可以先到这张表中来查一下

    • 如果是属于这张表中的权限,那么就需要进行运行时权限处理
    • 如果不在这张表中,那么只需要在 Androidmanifest.xml文件中添加一下权限声明就可以了。

    另外注意一下,表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名,但是用户一旦同意授权了,那么该权限所对应的权限组中所有的其他权限也会同时被授权。

    链接:更多关于系统权限

    1.2在程序运行时申请权限

    首先新建一个RuntimePermissionTest项目,我们就在这个项目的基础上来学习运行时权限的使用方法。在开始动手之前还需要考虑一下到底要申请什么权限,其实刚才表中列出的所有权限都是可以申请的,这里简单起见我们就使用CALL_PHONE这个权限来作为示例。

    CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的,因为拨打电话会涉及用户手机的资费问题,因而被列为了危险权限。在Android6.0系统出现之前,拨打电话功能的实现其实非常简单

    • 修改 activity_main.xml布局文件
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content" 
            android:id="@+id/make_call"
            android:text="Make Call"/>
        
    </LinearLayout>
    

    布局中定义一个按钮,用于触发打电话的逻辑

    • 修改MainActivity中的代码
    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){
                    try{
                        Intent intent = new Intent(Intent.ACTION_CALL);
                        intent.setData(Uri.parse("tel:10086"));
                        startActivity(intent);
                    }catch (SecurityException e){
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    

    在按钮的点击事件中:
    我们构建了一个隐式Intent
    Intent的action指定为Intent. ACTION_CALL,这是一个系统内置的打电话的动作
    data部分指定了协议是tel,号码是10086

    另外为了防止程序崩溃,我们将所有操作都放在了异常捕获代码块当中

    • 接下来修改 Androidmanifest.xml文件,在其中声明如下权限:

    <uses-permission android:name="android.permission.CALL_PHONE"/>

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.runtimepermissiontest">
    
        <uses-permission android:name="android.permission.CALL_PHONE"/>
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    

    这样我们就将拨打电话的功能成功实现了,并且在低于Android6.0系统的手机上都是可以正常运行的,但是如果我们在6.0或者更高版本系统的手机上运行,点击 Make Call1按钮就没有任何效果,这时观察logcat中的打印日志,你会看到如图所示的错误信息。


    图片.png

    错误信息中的Permission Denial提示我们错误是由权限被禁止所导致的,因为6.0及以上系统在使用危险权限时都必须进行运行时权限处理。

    下面我们来尝试修复这个问题

    • 修改MainActivity中的代码
    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){
            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()方法。
    checkSelfPermission()方法接收两个参数
    第一个参数是 Context,这个没什么好说的
    第二个参数是具体的权限名,比如打电话的权限名就是Manifest. permission.CALL_PHONE

    然后我们使用方法的返回值PackageManager.PERMISSION_GRANTED做比较
    相等就说明用户已经授权
    不等就表示用户没有授权。

    • 如果已经授权的话就直接去执行拨打电话的逻辑操作就可以了,这里我们把拨打电话的逻辑封装到了call()方法当中。

    • 如果没有授权的话,则需要调用Activitycompat.requestPermissions()方法来向用户申请授权
      requestPermissions()方法接收3个参数
      第一个参数要求是 Activity的实例
      第二个参数是一个 String数组,我们把要申请的权限名放在数组中即可
      第三个参数是请求码,只要是唯一值就可以了,这里传人了1。

    调用完了requestPermissions()方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同意或拒绝我们的权限申请,不论是哪种结果,最终都会回调到onRequestPermissionsResult()方法中,而授权的结果则会封装在grantResults参数当中。

    这里我们只需要判断一下最后的授权结果

    • 如果用户同意的话就调用call()方法来拨打电话
    • 如果用户拒绝的话我们只能放弃操作,并且弹出一条失败提示。

    现在重新运行一下程序,并点击Make Call按钮

    由于用户还没有授权过我们拨打电话权限,因此第一次运行会弹出这样一个权限申请的对话框,用户可以选择允许或者拒绝,比如说这里点击了“拒绝”,结果如图

    20190509_200937.gif

    由于用户没有同意授权,我们只能弹出一个操作失败的提示。
    下面我们再次点击Make Call按钮,仍然会弹出权限申请的对话框,这次点击“允许”,结果如图

    20190509_201053.gif

    可以看到,这次我们就成功进人到拨打电话界面了,并且由于用户已经完成了授权操作,之后再点击Make Call按钮就不会再弹出权限申请对话框了,而是可以直接拨打电话。那可能你会担心,万一以后我又后悔了怎么办?没有关系,用户随时都可以将授予程序的危险权限进行关闭。

    下面演示在小米手机 MIUI 10.8.8.30开发版下如何对权限进行更改:

    20190509_204232.gif

    有关运行时权限的内容就讲到这,下面进入正题——内容提供器

    2.访问其他程序中的数据

    内容提供器的用法一般有两种
    一种是使用现有的内容提供器来读取和操作相应程序中的数据
    另一种是创建自己的内容提供器给我们程序的数据提供外部访问接ロ。

    那么接下来我们就一个一个开始学习吧,首先从使用现有的内容提供器开始。

    如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。

    Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面我们就来看一看,内容提供器到底是如何使用的。

    2.1 ContentResolver的基本用法

    对于每一个应用程序来说,如果想要访问内容提供器中共享的数据就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取到该类的实例。

    ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中:
    insert()方法用于添加数据
    update()方法用于更新数据
    delete()方法用于删除数据
    query()方法用于查询数据

    不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替
    这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符

    它主要由两部分组成: authority i和path。

    authority是用于对不同的应用程序做区分的。一般为了避免冲突,都会采用程序包名的方式来进行命名。

    比如某个程序的包名是com. example.app,那么该程序对应的authority就可以命名为 com.example.app.provider

    path则是用于对同应用程序中不同的表做区分的,通常都会添加到authority的后面。

    比如某个程序的数据库里存在两张表: table1和table2,这时就可以将pah分别命名为/table1和/table2,然后把 authority和path进行组合,内容URI就变成了com.example.app.provider/table1和 com.example.app.provider/table2

    不过,目前还很难辦认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。
    因此,内容URI最标准的格式写法如下:


    图片.png

    有没有发现,内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。也正是因此, ContentResolver中的增改査方法才都接收Uri对象作为参数,因为如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。

    在得到了内容URI字符串之后,我们还需要将它解析成Uri对象オ可以作为参数传入。解析的方法也相当简单,代码如下:

    Uri uri = Uri.parse("content://com.example.app.provider/table1")

    只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象

    现在我们就可以使用这个Uri对象来查询tablel表中的数据了,代码如下所示

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

    这些参数和SQLiteDatabase中query()方法里的参数很像,但总体来说要简单些,毕竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句。

    下表对使用到的这部分参数进行了详细的解释。


    图片.png

    查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。
    读取的思路仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据,代码如下:

    if(cursor != null){
        while(cursor.moveToNext()){
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            int column2 =  cursor.getInt(cursor.getColumnIndex("column2"));
        }
        cursor.close();
    }
    

    掌握了最难的査询操作,剩下的增加、修改、删除操作就更不在话下了。
    我们先来看看如何向tablel表中添加一条数据,代码如下所示:

    ContentValues values = new ContentValues();
    values.put("column1","text");
    values.put("column2", 1);
    getContentResolver().insert(uri, values);

    可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传人即可。

    现在如果我们想要更新这条新添加的数据,把column1的值清空,可以借助ContentResolver的update()方法实现,代码如下所示:

    ContentValues values = new ContentValues();
    values.put( "column1" , "" );
    getContentResolver().update( uri, values, "column1 = ? and column2 = ?", new String[] { "text" , "1" } );

    注意上述代码使用了selection和 selectionArgs参数来对想要更新的数据进行约束,以防止所有的行都会受影响。

    最后,可以调用ContentResolver的delete()方法将这条数据删除掉,代码如下所示:

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

    到这里为止,我们就把ContentResolver中的增删改查方法全部学完了。

    那么接下来,我们就利用目前所学的知识,看一看如何读取系统电话簿中的联系人信息。

    2.2读取系统联系人

    首先我们需要在手机中添加联系人,方便后面进行读取
    由于我的这个实验机上还没有联系人,所以需要添加联系人,这里我们添加两个

    Screenshot_2019-05-10-15-27-20-508_com.android.co.png Screenshot_2019-05-10-15-31-44-121_com.android.co.png Screenshot_2019-05-10-15-32-02-070_com.android.co.png

    接下来新建一个ContactsTest项目。

    • 修改activity_main.xml代码
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/contacts_view">
            
        </ListView>
        
    </LinearLayout> 
    

    在布局中加入一个listview

    • 修改MainActivity中的代码
    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<String>(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()){
                        String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                        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){
            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:
            }
        }
    }
    
    

    oncreate()方法中

    • 我们首先获取了Listview控件的实例,并给它设置好了适配器
    • 然后开始调用运行时权限的处理逻辑,因为 READ_CONTACTS权限是属于危险权限的。关于运行时权限的处理流程相信你已经熟练掌握了
    • 在用户授权之后调用readContacts()方法来读取系统联系人信息。

    下面重点看一下readContacts()方法,可以看到

    • 这里使用了ContentResolver的query()方法来査询系统的联系人数据。不过传入的Uri参数怎么有些奇怪啊?为什么没有调用Uri.parse()方法去解析ー个内容URI字符串呢?这是因为ContactsContract.CommonDataKinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用Uri.parse()方法解析出来的结果。
    • 接着我们对Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出
      联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
      联系人手机号这一列对应的常量是ContactsContract.CommonDatakinds.Phone.NUMBER
    • 两个数据都取出之后,将它们进行拼接,然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下 ListView。
    • 最后千万不要忘记将Cursor对象关闭掉。

    这样就结東了吗?还差一点点,读取系统联系人的权限千万不能忘记声明。

    • 修改AndroidManifest.xml中的代码
    图片.png

    运行程序,查看效果


    20190510_161031.gif

    3.创建自己的内容提供器

    3.1创建内容提供器的步骤

    待更新...

    3.2实现跨程序共享

    待更新...

    相关文章

      网友评论

        本文标题:Android四大组件之内容提供器(Content Provid

        本文链接:https://www.haomeiwen.com/subject/ehkaoqtx.html