Android学习笔记之——Content Providers

之前博文《 Android学习笔记之——Android Studio的安装(3.6版本)、Java的基本语法及Android的概述 》曾经介绍过android有四大组件。本博文学习一下Content Providers(内容提供器)

 

目录

Android运行时 (Android runtime)权限

在程序运行时申请权限

内容提供器简介

访问其他程序中的数据

ContentResolver的基本用法

传入

查询

读取

添加

更新

删除

demo—读取系统联系人


 

 

Android运行时 (Android runtime)权限

Android开发团队在Android 6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私。在之前博文《 Android学习笔记之——Broadcast机制》中,当要访问系统的网络状态以及监听开机广播的时候,需要在AndroidManifest.xml文件中添加了这样两句权限声明:

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

因为访问系统的网络状态以及监听开机广播涉及了用户设备的安全性,因此必须在AndroidManifest.xml中加入权限声明,否则程序就会崩溃。

当有这样的声明后,对于低于6.0的系统安装界面会提醒权限,如果拒绝则无法安装程序。为此,6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如说一款相机应用在运行时申请了地理位置定位权限,就算拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。

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

Android中所有的危险权限

在程序运行时申请权限

新建一个RuntimePermissionTest项目(使用CALL_PHONE 这个权限),修改activity_main.xml布局文件,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/make_call"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Make Call" />

</androidx.constraintlayout.widget.ConstraintLayout>

我们在布局文件中只是定义了一个按钮,当点击按钮时就去触发拨打电话的逻辑。接着修改MainActivity中的代码,如下所示:

运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主去执行这些危险操作的。

package com.example.runtimepermissiontest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

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) {
                //首先通过是ContextCompat.checkSelfPermission() 方法看看用户是否授权了
                //第一个参数是Context
                // 第二个参数是具体的权限名,
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
                        permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {//相等为授权,此处为false为授权
                    //如果没有授权
                    //调用ActivityCompat.requestPermissions() 方法来向用户申请授权
                    //第一个参数要求是Activity的实例
                    //第二个参数是一个String 数组,我们把要申请的权限名放在数组中即可
                    //第三个参数是请求码,只要是唯一值就可以了,这里传入了1
                    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.ACTION_CALL是一个系统内置的打电话的动作(需要权限)
            intent.setData(Uri.parse("tel:10086"));//指定了协议是tel,号码是10086
            startActivity(intent);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

    //调用完了requestPermissions() 方法之后,系统会弹出一个权限申请的对话框,
    // 然后用户可以选择同意或拒绝我们的权限申请,不论是哪种结果,
    // 最终都会回调到onRequestPermissionsResult() 方法中
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions,int[] grantResults) {
        //授权的结果则会封装在grantResults 参数当中
        //requestCode是请求码,只要是唯一值就可以了,前面传入了1
        //permissions是要申请的权限

        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:
        }
    }

}

那么接下来修改AndroidManifest.xml文件,在其中声明如下权限:

<?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:roundIcon="@mipmap/ic_launcher_round"
        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>

结果如下所示

授权过一次,除非把程序卸载了,不然就默认授权了。

用户随时都可以将授予程序的危险权限进行关闭,进入Settings → Apps → RuntimePermissionTest → Permissions,界面如下图所示。

可以随时改变权限

 

内容提供器简介

对于一些可以让其他程序进行二次开发的基础性数据。例如系统的电话簿程序,它的数据库中保存了很多的联系人信息,如果这些数据都不允许第三方的程序进行访问的话,恐怕很多应用的功能都要大打折扣了。除了电话簿之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是内容提供器了。

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

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

访问其他程序中的数据

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。

ContentResolver的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver() 方法获取到该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中insert() 方法用于添加数据,update() 方法用于更新数据,delete() 方法用于删除数据,query() 方法用于查询数据。

ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri 参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

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

传入

在得到了内容URI字符串之后,我们还需要将它解析成Uri 对象才可以作为参数传入。

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

查询

只需要调用Uri.parse() 方法,就可以将内容URI字符串解析成Uri 对象了。现在我们就可以使用这个Uri 对象来查询table1表中的数据了,代码如下所示:

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

读取

查询完成后返回的是一个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();
}

添加

何向table1表中添加一条数据,代码如下所示:

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" });

 

demo—读取系统联系人

先创建一系列的联系人

接下来新建一个ContactsTest项目。希望读取出来的联系人信息能够在ListView中显示,因此,修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </ListView>

</androidx.constraintlayout.widget.ConstraintLayout>

接着修改MainActivity中的代码,如下所示:

package com.example.contactstest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

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控件的实例
        ListView contactsView = (ListView) findViewById(R.id.contacts_view);

        //设置适配器对应listview
        adapter = new ArrayAdapter<String>(this, android.R.layout. simple_list_item_1, contactsList);
        contactsView.setAdapter(adapter);

        //跟上一part一样,检测权限
        //调用运行时权限的处理逻辑
        //通过是ContextCompat.checkSelfPermission() 方法看看用户是否授权了
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            //若没有授权
            //调用ActivityCompat.requestPermissions() 方法来向用户申请授权
            ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.READ_CONTACTS }, 1);
        } else {
            //授权了,则调用readContacts()方法,通过Content Providers来获取数据
            readContacts();
        }
    }

    //读取联系人的数据信息
    private void readContacts() {
        Cursor cursor = null;
        try {//首先整个代码放在异常处理内。
            //使用了ContentResolver的query()方法来查询联系人数据
            //首先查询后,返还的是一个Cursor 对象,然后就可以将数据从Cursor 对象中逐个读取出来
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            //其中的ContactsContract.CommonDataKinds.Phone类封装好提供了一个CONTENT_URI 常量。
            // 这个常量就是使用Uri.parse() 方法解析出来的结果
            //内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据

            //将数据从Cursor 对象中逐个读取出来了。
            // 读取的思路仍然是通过移动游标的位置来遍历Cursor 的所有行,
            // 然后再取出每一行中相应列的数据,
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    // 获取联系人姓名
                    String displayName = cursor.getString(cursor.getColumnIndex
                            (ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    //联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME

                    // 获取联系人手机号
                    String number = cursor.getString(cursor.getColumnIndex
                            (ContactsContract.CommonDataKinds.Phone.NUMBER));
                    //联系人手机号这一列对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER

                    //两个数据都取出之后
                    // 将它们进行拼接,并且在中间加上换行符
                    contactsList.add(displayName + "\n" + number);
                }
                adapter.notifyDataSetChanged();//并通知刷新一下ListView
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();//最后需要关掉Cursor 对象
            }
        }
    }

    @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:
        }
    }
}

最后修改AndroidManifest.xml中的代码,声明联系人的权限,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.contactstest">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        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>

结果如下图所示

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值