Android ContentProvider

ContentProvider

contentprovider是android四大组件之一的内容提供器,它主要的作用就是将程序的内部的数据和外部进行共享,为数据提供外部访问接口,被访问的数据主要以数据库的形式存在,而且还可以选择共享哪一部分的数据。这样一来,对于程序当中的隐私数据可以不共享,从而更加安全。contentprovider是android中一种跨程序共享数据的重要组件。

使用系统的ContentProvider

系统的ContentProvider有很多,如通话记录,短信,通讯录等等,都需要和第三方的app进行共享数据。既然是使用系统的,那么contentprovider的具体实现就不需要我们担心了,使用内容提供者的步骤如下

  • 获取ContentResolver实例
  • 确定Uri的内容,并解析为具体的Uri实例
  • 通过ContentResolver实例来调用相应的方法,传递相应的参数,但是第一个参数总是Uri,它制定了我们要操作的数据的具体地址

可以通过读取系统通讯录的联系人信息,显示在Listview中来实践这些知识。不要忘记在读取通讯录的时候,在清单文件中要加入相应的读取权限。

自定义ContentProvider

步骤:

  1. 定义继承自ContentProvider的类。
  2. 重写这个类中的抽象方法。
  3. 在清单文件中注册。
抽象方法 解释
oncreate ContentProvider创建时所执行的一个回调方法,负责数据库的创建和更新操作。
query
update
insert
delete
gettype 获取我们通过参数传递进去的Uri的MIME类型。

Uri的形式解析,一般有两种:

  • 以路径名为结尾,这种Uri请求的是整个表的数据,如: content://com.demo.androiddemo.provider/tabl1 标识我们要访问tabl1表中所有的数据
  • 以id列值结尾,这种Uri请求的是该表中和其提供的列值相等的单条数据。 content://com.demo.androiddemo.provider/tabl1/1 标识我们要访问tabl1表中_id列值为1的数据。

示例(Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
* 第一行代码第三版
*
* CP 主要用于在不同应用之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,
* 同时还能保证被访问数据的安全性。目前,使用 CP 是 Android 实现跨程序共享数据的标准方式。
*
* 不同于文件存储和 SharedPreferences 存储中的两种全局可读写操作模式,CP 可以选择只对哪一部分数据进行共享,
* 从而保证程序中的隐私数据不会有泄露风险。
*
* CP 的用法一般有两种:
* 使用现有 CP 读取和操作相应程序中的数据。(在自己的程序中访问其他应用程序的数据,获得该应用程序的内容 URI,借助 ContentResolver 进行操作)
* 创建自己的 CP,给程序的数据提供外部访问接口。(新建 MyProvider 类去继承 ContentProvider,并重写全部的 6 个方法)
*/
class ContentProviderActivity:BaseActivity() {

/**
* 想要访问 CP 中共享的数据,一定要借助 ContentResolver 类。
* 通过 Context 中的 getContentResolver() 获取 ContentResolver 类的实例。
* ContentResolver 类中提供了一系列的方法用于对数据进行增删改查:insert()、update()、delete()、query()。
*
* 不同于 SQLite ,CP 中的增删改查方法都是不接收表名参数的,而是使用一个 Uri 参数代替,这个参数被称为内容 URI。
* 内容 URI 给 ContentProvider 中的数据建立了唯一标识符,它主要由两部分组成:authority 和 path。
* authority 用于对不同应用程序作区分,一般为了避免冲突,会采用应用包名的方式进行命名。
* 比如此应用对应的 authority 就可以命名为 com.example.myapplication.provider。
* path 是用于对同一应用程序中不同的表做区分,通常会添加到 authority 的后面。
* 比如某个应用的数据库里存在两张表,table1 和 table2,这时就可以将 path 分别命名为 /table1 和 /table2,
* 然后把 authority 和 path 进行组合,内容 URI 就变成了:
* com.example.myapplication.provider/table1 和 com.example.myapplication.provider/table2。
* 为了容易辨识,还需要在字符串的头部加上协议声明,最标准的内容 URI 格式如下:
* content://com.example.myapplication.provider/table1
* content://com.example.myapplication.provider/table2
* 得到了内容 URI 字符串后,还需要将它解析成 Uri 对象才可以作为参数传入,解析的方法如下:
* val uri = Uri.parse("content://com.example.myapplication.provider/table1")
*
* 使用这个 Uri 对象查询 table1 表中的数据:
* // 参数 1,指定查询某个应用程序下的某一张表
* // 参数 2,指定查询的列名
* // 参数 3,指定 where 的约束条件
* // 参数 4,为 where 中的占位符提供具体的值
* // 参数 5,指定查询结果的排列方式
* val cursor = contentResolver.query(uri,projection,selection,selectionArgs,sortOrder)
* 查询完成后,返回的仍然是一个 Cursor 对象。
* 查询操作:
* while(cursor.moveToNext()){
* val column1 = cursor.getString(cursor.getColumnIndex("column1"))
* val column2 = cursor.getString(cursor.getColumnIndex("column2"))
* }
* cursor.close()
* 向 table1 表中添加一条数据:
* val values = contentValuesOf("column1" to "text","column2" to 1)
* contentResolver.insert(uri, values)
* 更新操作:
* val values = contentValuesOf("column1" to "")
* // 使用 selection 和 selectionArgs 参数对想要更新的数据进行约束,防止所有的行都受影响。
* contentResolver.update(uri, values,"column1 = ? and column2 = ?",arrayOf("text","1"))
* 删除操作:
* contentResolver.delete(uri, "column2 = ?",arrayOf("1"))
*/

private val contactsList = ArrayList<String>()
private lateinit var adapter:ArrayAdapter<String>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contentprovider)

initData()
}

private fun initData() {
// ListView
adapter = ArrayAdapter(this,android.R.layout.simple_list_item_1,contactsList)
lvContacts.adapter = adapter

if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED){

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

/**
* 查询系统联系人数据
* 权限:<uses-permission android:name="android.permission.READ_CONTACTS" />
*/
private fun readContacts() {
// 这里 ContactsContract.CommonDataKinds.Phone 类做好了封装,提供了一个 CONTENT_URI 常量,
// 所以不需要再去解析内容 URI 字符串。
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,null,null,null)?.apply {
while(moveToNext()){
// 获取联系人姓名
val displayName = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
// 获取联系人手机号
val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contactsList.add("$displayName\n$number")
}
adapter.notifyDataSetChanged()
close()
}
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, ContentProviderActivity::class.java)
context.startActivity(intent)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/**
* 提供外部访问接口
*/
class DatabaseProvider : ContentProvider() {

private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.databasetest.provider"
private var dbHelper: MyDatabaseHelper?=null

/**
* by lazy 代码块是 Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,
* 只有当 uriMatcher 变量首次被调用时才会执行,并且会将代码块中最后一行代码的返回值赋给 uriMatcher。
*/
private val uriMatcher by lazy {
val matcher = UriMatcher(UriMatcher.NO_MATCH)
matcher.addURI(authority,"book",bookDir)
matcher.addURI(authority,"book/#",bookItem)
matcher.addURI(authority,"category",categoryDir)
matcher.addURI(authority,"category/#",categoryItem)
matcher
}

/**
* 综合利用了 Getter 方法语法糖、?. 操作符、let 函数、?: 操作符、以及单行代码函数语法糖。
* 首先调用 getContext() 并借助 ?. 操作符和 let 函数判断它的返回值是否为空:
* 如果为空就使用 ?: 操作符返回 false,表示 CP 初始化失败,如果不为空,就执行 let 函数中的代码。
* ------------------------------------------------------------------------------
* 在 let 函数中创建了一个 MyDatabaseHelper 实例,
* 然后返回 true 表示 CP 初始化成功
* ------------------------------------------------------------------------------
* 由于是借助多个操作符和标准函数,因此这段逻辑是在一行表达式内完成的,符合单行代码函数的语法糖要求,
* 所以直接用等号连接返回值即可。
* ------------------------------------------------------------------------------
* 其他几个方法的语法结构类似
*/
override fun onCreate() = context?.let{

dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
true
}?:false


override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = dbHelper?.let {
// 删除数据
val db = it.readableDatabase
val deletedRows = when(uriMatcher.match(uri)){
bookDir -> db.delete("Book",selection,selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.delete("Book","id = ?", arrayOf(bookId))
}
categoryDir -> db.delete("Category",selection,selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.delete("Category","id = ?", arrayOf(categoryId))
}
else -> 0
}
deletedRows
}?: 0

override fun insert(uri: Uri, values: ContentValues?) = dbHelper?.let {
// 添加数据
val db = it.writableDatabase
val uriReturn = when(uriMatcher.match(uri)){
bookDir,bookItem -> {
val newBookId = db.insert("Book",null,values)
Uri.parse("content://$authority/book/$newBookId")
}
categoryDir,categoryItem -> {
val newBookId = db.insert("Category",null,values)
Uri.parse("content://$authority/category/$newBookId")
}
else -> null
}
uriReturn
}


override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?) = dbHelper?.let {
// 查询数据
val db = it.readableDatabase
val cursor = when(uriMatcher.match(uri)){
bookDir -> {
db.query("Book",projection,selection,selectionArgs,null,null,sortOrder)
}
bookItem -> {
// Uri 对象的 getPatchSegments(),它会将内容 URI 权限之后的部分以 "/" 符号进行分割,
// 并把分割后的结果放入一个字符串列表中,这个列表的第 0 个位置存放的是路径,第 1 个位置是 id。
val bookId = uri.pathSegments[1]
db.query("Book",projection,"id = ?", arrayOf(bookId),null,null,sortOrder)
}
categoryDir -> {
db.query("Category",projection,selection,selectionArgs,null,null,sortOrder)
}
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.query("Category",projection,"id = ?", arrayOf(categoryId),null,null,sortOrder)
}
else -> null
}
cursor
}

override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?) = dbHelper?.let{
// 更新数据
val db = it.writableDatabase
val updatedRows = when(uriMatcher.match(uri)){
bookDir -> db.update("Book",values,selection,selectionArgs)
bookItem -> {
val bookId = uri.pathSegments[1]
db.update("Book",values,"id = ?", arrayOf(bookId))
}
categoryDir -> db.update("Category",values,selection,selectionArgs)
categoryItem -> {
val categoryId = uri.pathSegments[1]
db.update("Category",values,"id = ?", arrayOf(categoryId))
}
else -> 0
}
updatedRows
}?: 0


override fun getType(uri: Uri) = when(uriMatcher.match(uri)){
bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
else -> null
}
}


/**
* 其它应用程序调用此接口的实例
*/
//class MainActivity : AppCompatActivity() {
//
// var bookId: String? =null
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
//
// btnAdd.setOnClickListener{
// // 添加数据
// // 将内容 URI 解析成 Uri 对象
// val uri = Uri.parse("content://com.example.databasetest.provider/book")
// // 将要添加的数据存放在 ContentValues 对象中
// val values = contentValuesOf("name" to "A Clash of Kings",
// "author" to "George Martin","pages" to 1040,"price" to 22.85)
// val newUri = contentResolver.insert(uri,values)
// // insert() 会返回一个 Uri 对象,其中包含了新增数据的 id。
// bookId = newUri?.pathSegments?.get(1)
// }
// btnQuery.setOnClickListener{
// // 查询数据
// val uri = Uri.parse("content://com.example.databasetest.provider/book")
// contentResolver.query(uri,null,null,null,null)?.apply {
// while (moveToNext()){
// val name = getString(getColumnIndex("name"))
// val author = getString(getColumnIndex("author"))
// val pages = getInt(getColumnIndex("pages"))
// val price = getDouble(getColumnIndex("price"))
// Log.e("TAG","name is $name")
// Log.e("TAG","author is $author")
// Log.e("TAG","pages is $pages")
// Log.e("TAG","price is $price")
// }
// close()
// }
// }
// btnUpdate.setOnClickListener{
// // 更新数据
// // 在尾部增加了一个 id,避免影响其他行
// bookId?.let {
// val uri = Uri.parse("content://com.example.databasetest.provider/book/$it")
// val values = contentValuesOf("name" to "A Storm of Swords",
// "pages" to 1216,"price" to 24.05)
// contentResolver.update(uri,values,null,null)
// }
// }
// btnDelete.setOnClickListener{
// // 删除数据
// // 在尾部增加了一个 id,避免影响其他行
// bookId?.let {
// val uri = Uri.parse("content://com.example.databasetest.provider/book/$it")
// contentResolver.delete(uri,null,null)
// }
// }
// }
//}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class MyDatabaseHelper(val context: Context, name:String, version:Int):
SQLiteOpenHelper(context,name,null,version){

/**
* integer 表示整型
* real 表示浮点型
* text 表示文本类型
* blob 表示二进制类型
* primary key 将 id 列设为主键,并用 autoincrement 关键字表示 id 列是自增长的。
*/

/**
* 第二版
*/
// private val createBook = "create table Book(" +
// "id integer primary key autoincrement," +
// "author text," +
// "price real," +
// "pages integer," +
// "name text)"

/**
* 第三版,新加字段,用来和 Category 表建立关联。
*/
private val createBook = "create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text," +
"category_id integer)"

private val createCategory = "create table Category(" +
"id integer primary key autoincrement," +
"category_name text," +
"category_code integer)"

override fun onCreate(db: SQLiteDatabase) {
db.execSQL(createBook)
db.execSQL(createCategory)

// 跨进程访问时不能直接使用 Toast
// Toast.makeText(context,"Create succeeded",Toast.LENGTH_SHORT).show()
}


/**
* 升级数据库的最佳写法
*
* 注意,每当升级一个数据库版本时,onUpgrade() 里都一定要写一个相应的 if 判断语句,保证 App 在跨版本升级时,每一次的数据库修改都能被全部执行。
*/
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d("TAG","oldVersion is $oldVersion")
// 现版本 version 为 3,oldVersion 为 2.
// 如果直接安装最新版程序,则会走 onCreate(),不会走 onUpgrade()。
// 如果是从 version 2 升级为 version 3 版本,则会走 onUpgrade(),并且执行下面的语句来升级数据库。

// 当用户直接安装第二版程序时,就会进入 onCreate(),将两张表一起创建,
// 如果用户使用第二版程序覆盖第一版的程序时,就会进入升级数据库的操作中,只需创建一个表就可以。
if (oldVersion <= 1){
db.execSQL(createCategory)
}

if (oldVersion <= 2){
db.execSQL("alter table Book add column category_id integer")
}

// 暴力写法,开发阶段还可以,上线不行。
// 执行了两条 DROP 语句,如果发现数据库中已经存在 Book 表或 Category 表,就将它们删除,然后调用 onCreate() 重新创建。
// db.execSQL("drop table if exists Book")
// db.execSQL("drop table if exists Category")
// onCreate(db)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* 很多方法里带有 uri 参数,这个参数也正是调用 ContentProvider 的增删改查方法时传递过来的。
* 这里则需要对传入的 uri 参数进行解析,从中分析出调用方期望访问的表和数据。
* ----------------------------------------------------------------------------------------------------
* 标准的内容 URI 写法:表示调用方期望访问的是 com.example.myapplication 这个应用的 table1 表中的数据。
* content://com.example.myapplication.provider/table1
* 还可以在后面加上一个 id:表示调用方期望访问的是表中 id 为 1 的数据。
* content://com.example.myapplication.provider/table1/1
* ----------------------------------------------------------------------------------------------------
* 内容 URI 的格式主要就只有以上两种,以路径结尾表示期望访问该表中所有数据,以 id 结尾表示期望访问该表中拥有相应 id 的数据。
* 可以使用通配符分别匹配这两种格式的内容 URI,规则如下:
* * 表示匹配任意长度的任意字符。
* # 表示匹配任意长度的数字。
* 所以,一个能够匹配任意表的内容 URI 格式如下:
* content://com.example.myapplication.provider/星号
* 一个能够匹配 table1 表中任意一行数据的内容 URI 格式如下:
* content://com.example.myapplication.provider/table1/#
* ----------------------------------------------------------------------------------------------------
* 接着借助 UriMatcher 类实现匹配内容 URI 的功能,它提供了一个 addURI(),
* 接收 3 个参数,可以分别把 authority、path 和一个自定义代码传进去。
* 这样,当调用 UriMatcher 的 match() 时,就可以将一个 Uri 对象传入,返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,
* 利用这个代码,就可以判断出调用方期望访问的是哪张表中的数据了。
* ----------------------------------------------------------------------------------------------------
* 对于安全问题,因为所有的增删改查操作都一定要匹配到相应的内容 URI 格式才能进行,
* 而我们当然不可能向 UriMatcher 中添加隐私数据的 URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了。
*/

class MyProvider: ContentProvider() {

private val tableDir = 0
private val tableItem = 1
private val table2Dir = 2
private val table2Item = 3

private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

/**
* MyProvider 类实例化时,立刻创建了 UriMatcher 类的实例,并调用 addURI(),将期望匹配的内容 URI 格式传递进去。
*/
init {
uriMatcher.addURI("com.example.myapplication.provider","table",tableDir)
uriMatcher.addURI("com.example.myapplication.provider","table/#",tableItem)
uriMatcher.addURI("com.example.myapplication.provider","table2",table2Dir)
uriMatcher.addURI("com.example.myapplication.provider","table2/#",table2Item)
}

/**
* 查询的结果存放在 Cursor 对象中返回
* uri 参数指定哪张表
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
// 通过 match() 对传入的 Uri 对象进行匹配,
// 如果发现 UriMatcher 中某个内容 URI 格式成功匹配了该 Uri 对象, 则会返回相应的自定义代码,
// 然后就可以判断出调用方期望访问的数据了。
// 其他方法的实现类似。
when(uriMatcher.match(uri)){
tableDir -> {
// 查询 table1 表中的所有数据
}
tableItem -> {
// 查询 table1 表中的单条数据
}
table2Dir -> {
// 查询 table2 表中的所有数据
}
table2Item -> {
// 查询 table2 表中的单条数据
}
}
return null
}

/**
* 添加完成后,返回一个用于表示这条新记录的 URI。
*/
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}

/**
* 初始化 CP 时调用,通常用来完成对数据库的创建和升级等操作。
* 返回 true 表示初始化成功,返回 false 表示失败。
*/
override fun onCreate(): Boolean {
return false
}

/**
* 受影响的行数将作为返回值返回
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}

/**
* 被删除的行数将作为返回值返回
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}

/**
* 根据传入的内容 URI 返回相应的 MIME 类型。
* 此方法是所有 CP 都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。
* 一个内容 URI 所对应的 MIME 字符串主要由 3 部分组成,Android 对这 3 个部分做了如下规定:
* 必须以 vnd 开头。
* 如果内容 URI 以路径结尾,则后接 android.cursor.dir/;如果内容 URI 以 id 结尾,则后接 android.cursor.item/。
* 最后接上 vnd.<authority>.<path>。
* -------------------------------------------------------------------------------------------------
* content://com.example.myapplication.provider/table1 所对应的 MIME 类型为:
* vnd.android.cursor.dir/vnd.com.example.myapplication.provider.table1
* ------------------------------------------------------------------------------------------------
* content://com.example.myapplication.provider/table1/1 所对应的 MIME 类型为:
* vnd.android.cursor.item/vnd.com.example.myapplication.provider.table1
*/
override fun getType(uri: Uri) = when(uriMatcher.match(uri)){
tableDir -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.table1"
tableItem -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.table1"
table2Dir -> "vnd.android.cursor.dir/vnd.com.example.myapplication.provider.table2"
table2Item -> "vnd.android.cursor.item/vnd.com.example.myapplication.provider.table2"
else -> null
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 运行时权限的核心,就是在程序运行过程中,由用户授权我们去执行某些危险操作。
*/
class PermissionActivity :BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_permission)

// 申请运行时权限
btnCall.setOnClickListener{
// 先判断用户是否已经给过授权了,借助的是 ContextCompat.checkSelfPermission(),接收两个参数:
// 第一个参数是 Context。
// 第二个参数是具体的权限名,比如打电话的权限名是 Manifest.permission.CALL_PHONE。
// 然后使用方法的返回值和 PackageManager.PERMISSION_GRANTED 比较,相等就说明用户已经授权。
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSION_GRANTED){
// 如果不相等,说明未授权,则需要调用 ActivityCompat.requestPermissions() 向用户申请授权,它接收三个参数:
// 第一个参数要求是 Activity 的实例
// 第二个参数是一个 String 数组,把要申请的权限名放在数组中即可。
// 第三个参数是请求码,只要是唯一值就可以。
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CALL_PHONE),1)
}else {
call()
}
}
}

/**
* 调用完 requestPermissions() 后,系统会弹出一个申请权限的对话框,不论是否同意,最终都会回调到这个方法中。
* 授权的结果会封装在 grantResults 参数中。
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out 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(){
// 打开拨号界面,不需要声明权限.
// val intent = Intent(Intent.ACTION_DIAL)

// 直接拨打电话,危险权限.清单文件声明和动态申请权限,都需要.
// <uses-permission android:name="android.permission.CALL_PHONE" />
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:1008611")
startActivity(intent)
}catch (e: SecurityException){
e.printStackTrace()
}

}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, PermissionActivity::class.java)
context.startActivity(intent)
}
}
}
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
<provider
android:name=".contentprovider.DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true"></provider> <!-- exported: 是否允许外部程序访问我们的 CP,enabled:表示是否启用这个 CP。 -->

<provider
android:name=".contentprovider.MyProvider"
android:authorities="com.example.myapplication.provider"
android:enabled="true"
android:exported="true" />