一、Android权限机制:

Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限。

普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作。

危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信
息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。

危险权限一览:

Android危险权限安装权限 安卓危险权限有哪些_Android危险权限安装权限

二、在程序运行时申请权限

以拨打电话权限为例。修改AndroidManifest.xml文件,在其中声明如下权限:

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

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

需要修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val callButton: Button = findViewById(R.id.callButton)
        callButton.setOnClickListener {
            if (ContextCompat.checkSelfPermission(this,
                    android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                    arrayOf(android.Manifest.permission.CALL_PHONE), 1)
            } else {
                call()
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call()
                } else {
                    Toast.makeText(this, "You denied the permission",
                        Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun call() {
        try {
            val intent = Intent(Intent.ACTION_CALL)
            intent.data = Uri.parse("tel:10086")
            startActivity(intent)
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }
}

上面的代码覆盖了运行时权限的完整流程!!!

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

首先要判断用户是否给过我们授权了:借助 ContextCompat.checkSelfPermission()  方法,第一个参数是context,第二个参数是具体的权限名,比如打电话的权限是:android.Manifest.permission.CALL_PHONE。

然后使用方法的返回值与  PackageManager.PERMISSION_GRANTED  进行比较,如果相同则表明已授权,否则表示没有授权。

如果已经授权的话就简单了,直接执行拨打电话的逻辑操作就可以了;

如果没有授权的话,则需要调用  ActivityCompat.requestPermissions()  方法向用户申请授权。该方法接受三个参数,第一个参数是Activity实例;第二个参数是一个String数组,需要把申请的权限名放在数组即可;第三个参数是请求码,只要是唯一值就行,这里传入1。

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

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    )

这里我们只需要判断一下最后的授权结果:如果用户同意的话,就调用call()方法拨打电话;如果用户拒绝的话,我们只能放弃操作,并且弹出一条失败提示。这里使用if语句来进行判断,当requestCode为1时,则进行判断  grantResults  是否为空,以及是否和 PackageManager.PERMISSION_GRANTED 相等。

Android危险权限安装权限 安卓危险权限有哪些_ide_02

三、访问其他程序中的数据

ContentProvider的用法一般有两种:

一种是通过ContentProvider读取和操作相应程序的数据;

另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。

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

ContentResolver的基本用法

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

不同于SQLiteDatabase,ContentResolver的增删改查方法不接收表名参数,而是接收一个Uri参数。URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成: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/table1com.example.app.provider/table2

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

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

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

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

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

val cursor = contentResolver.query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder)

contentResolver的query与SQLite的参数很像,他们的对应关系是:

Android危险权限安装权限 安卓危险权限有哪些_ide_03

 query() 返回的对象也是一个Cursor对象,可以通过移动游标位置遍历 Cursor的所有行。

while (cursor.moveToNext()) {
    val column1 = cursor.getString(cursor.getColumnIndex("column1"))
    val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()

其它的增删改代码如下:

val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values)
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))

与SQLite的区别只有第一个参数,表名变成了uri!!!

四、创建自己的ContentProvider

ContentProvider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写!!!!

class MyProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        return false
    }
    override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                       selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        return null
    }
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }
    override fun update(uri: Uri, values: ContentValues?, selection: String?,
                        selectionArgs: Array<String>?): Int {
        return 0
    }
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }
    override fun getType(uri: Uri): String? {
        return null
    }
}

(1)、 onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
(2) 、query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
(3) 、insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。
(4) 、update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。
(5)、 delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
(6) 、getType()。根据传入的内容URI返回相应的MIME类型。 

4.1 创建ContentProvider步骤

标准uri写法:

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

表示希望访问 com.example.app  这个应用的table1表中的数据。

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

表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据

内容URI的格式主要就只有以上两种,以路径结尾表示期望访问该表中所有的数据,以id结尾表示期望访问该表中拥有相应id的数据。我们可以使用通配符分别匹配这两种格式的内容URI,规则如下。

一个能够匹配任意表的内容URI格式就可以写成:

content://com.example.app.provider/*

一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:

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

然后再借助 UriMatcher 这个类就可以轻松地实现匹配内容URI的功能。

UriMatcher 提供了addURI()  方法,该方法接受三个参数:可以分别把authority、path一个自定义代码传进去。这

当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,可以根据代码判断出调用方期望访问的是哪张表的数据!!!

class MyProvider : ContentProvider() {

    private val table1Dir = 0
    private val table1Item = 1
    private val table2Dir = 2
    private val table2Item = 3

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

    companion object{
        const val AUTHORITY = "com.example.app.provider"
    }

    init {
        uriMatcher.addURI(AUTHORITY, "table1", table1Dir)
        uriMatcher.addURI(AUTHORITY, "table1/#", table1Item)
        uriMatcher.addURI(AUTHORITY, "table2", table2Dir)
        uriMatcher.addURI(AUTHORITY, "table2/#", table2Item)
    }

    .......

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        when (uriMatcher.match(uri)) {
            table1Dir -> {

            }
            table1Item -> {

            }
            table2Dir -> {

            }
            table2Item -> {

            }
        }
        return null
    }
    ......
}

MyProvider中新增了4个整型变量,作为自定义代码!!!其中table1Dir表示访问table1表中的所有数据,table1Item表示访问table1表中的单条数据,table2Dir表示访问table2表中的所有数据,table2Item表示访问table2表中的单条数据。

接着创建了UriMatcher实例,并调用 addUri()  方法,将要匹配的内容URI格式传递进入,这里可以匹配通配符。

query() 调用时,会通过  UriMatcher  的  match()   方法对传入的uri对象进行匹配,如果匹配了该Uri对象,则会返回相应的自定义代码,然后就可以判断断出调用方期望访问的到底是什么数据了。

insert()、update()、delete()这几个方法的实现是差不多的,它们都会携带uri这个参数,然后同样利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。

——————————————————————————————————————

除了增删改查以及创建五个函数,还有一个  getType()  方法。

它是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。

URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。

(1)、必须以vnd开头。
(2)、如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/。
(3)、最后接上vnd.<authority>.<path>。

所以,对于content://com.example.app.provider/table1  (目录)这个内容URI和于content://com.example.app.provider/table1/1(以id) 写成MIME格式可以写成:

override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
        table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
        table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
        table2Dir -> "vnd.android.cursor.Dir/vnd.com.example.app.provider.table2"
        table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
        else -> null
    }

如何才能保证隐私数据不会泄漏出去呢?

其实多亏了ContentProvider的良好机制,这个问题在不知不觉中已经被解决了。因为所有的增删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。

4.2 跨程序数据共享