Android中跨程序共享数据 您所在的位置:网站首页 android11读取contentprovider Android中跨程序共享数据

Android中跨程序共享数据

2024-06-26 21:31| 来源: 网络整理| 查看: 265

说明: 本文是郭霖《第一行代码-第3版》的读书笔记

之前介绍的持久化技术保存的数据只能在当前应用程序中访问,虽然也提供了操作模式如MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,但由于安全性问题被弃用。现在Android官方更推荐使用ContentProvider技术。

8.1 ContentProvider简介

ContentProvider主要用于不同应用程序之间实现数据共享功能。它提供了一套完整的机制,允许一个程序访问另一个程序的数据,且能保证安全性。不同于持久化技术,ContentProvider可以选择只对哪一部分的数据进行共享。

8.2 运行时权限

Android的权限机制从一开始就存在了,但由于一些软件存在店大欺客的现象,Android团队在Android 6.0系统中引入了运行时权限这个功能,以更好保护用户的安全和隐私。

8.2.1 Android权限机制详解

在BroadcastReceiver为了接收开机广播时,需要在Android.xml中加入权限声明,否则程序就会崩溃。权限声明的作用在于:

安装界面会给出权限使用提醒用户随时可以在管理界面查看任意程序的权限申请状况

由于软件普遍存在滥用权限的现象,因此Android团队加入了运行时权限的功能,即用户不需要在软件安装的时候一次性授权所有的权限,而是可以在软件的使用过程中再对某一权限进行授权,即使不同意这个权限导致无法使用这个功能,也不影响其他功能的使用。

常用权限大概可以分为两类,一类是普通权限,一类是危险权限(还有特殊权限,但用的较少),普通权限是指那些不会直接威胁到用户的安全和隐私的权限,系统会自动帮我们授权,而对于那些危险权限,如联系人信息、位置信息等,需要用户手动授权才可以,否则程序无法使用相应的功能。

下表中给出了危险权限表(表有更新),除了危险权限之外,其余就大多是普通权限了。

android危险权限列表- CodeAntenna

**注意:**表格中每个危险权限都属于一个权限组,进行授权时使用的是权限名,原则上,用户一旦同意某个权限申请之后,同组的其他权限也会被系统自动授权,但不要基于此规则实现功能逻辑。

8.2.2 在程序允许时申请权限

这里以CALL_PHONE这个权限为例,这里在布局中加入一个Button,点击按钮就实现拨打电话的逻辑:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button:Button = findViewById(R.id.makeCall) button.setOnClickListener { try { val intent = Intent(Intent.ACTION_CALL) intent.data = Uri.parse("tel:10086") startActivity(intent) } catch (e: SecurityException) { e.printStackTrace() } } } }

之后还需要在AndroidManifest.xml文件中声明如下权限,否则程序会崩溃:

点击MakeCall按钮,会出现如下错误:java.lang.SecurityException: Permission Denial: starting Intent,这是由于权限被禁止导致的,所以使用权限时必须进行运行时权限处理。

下面使用运行时权限的方法,修改MainActivity:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button:Button = findViewById(R.id.makeCall) button.setOnClickListener { // 检查是否已授权 if (ContextCompat.checkSelfPermission(this, "android.permission.CALL_PHONE") != PackageManager.PERMISSION_GRANTED) { // 若未授权则申请授权 ActivityCompat.requestPermissions(this, arrayOf("android.permission.CALL_PHONE"), 1) } else { call() } } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, 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 permisson", 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(),判断该方法的返回值是否等于PackageManager.PERMISSION_GRANTED。相等说明已授权。这个方法需要传入两个参数:Context和具体的权限名。这个具体的权限名既可以填"android.permission.CALL_PHONE"(Android官方),也可以给Manifest.permisson.CALL_PHONE。如果已授权,则直接调用封装好的call()方法,如果未授权,则需要调用ActivityCompat.requestPermissons()方法向用户申请授权。这个方法接收三个参数:Activity的实例,申请权限名的数组(把权限名放入arrayof()即可),请求码(唯一即可)。调用这个方法后会弹出一个权限申请的对话框,用户可以选择Allow或者Deny。用户选择了权限申请的对话框选项后,会回调到onRequestPermissonsResult()方法中,授权结果会封装在grantResults参数中,这里我们只需要判断一下最后的授权结果:grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED。

如果用户已经授权拨打电话的权限,则后面不会再弹出权限申请的对话框,但用户也可以随时在设置中将这项权限关闭。

8.3 访问其他程序中的数据

ContentProvider的用法一般有两种,一种是结合现有的ContentProvider读取和操作相应程序中的数据,另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口。

如果一个程序通过ContentProvider对其数据提供了外部访问接口,则其他程序都可以对这部分数据进行访问。Android中自带的通讯录、短信、媒体库等程序都提供了类似的访问接口。

8.3.1 ContentResolver的基本用法

如果要访问ContentProvider中共享的数据,则一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列方法用于数据的增删改查操作。但ContentProvider不接收表名参数,而是使用一个Uri参数作为代替。Uri由authority和path组成。例如一个Uri为"content://com.example.app.provider/table1",这里content://是协议声明,com.example.app.provider是authority,/table1是path。这就是内容Uri字符串,要将其转换成Uri对象,只需调用Uri.parse()方法即可

8.3.2 读取系统联系人

首先在布局中添加一个ListView,然后修改MainActivity的代码如下:

class MainActivity : AppCompatActivity() { private val contactsList = ArrayList() private lateinit var adapter: ArrayAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 设置适配器 adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList) contactsView.adapter = adapter // 判断权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // 如果不满足要申请权限 ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1) } else { readContacts() } } @SuppressLint("Range") private fun readContacts() { // 查询联系人数据 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() } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, 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() } } } } }

首先定义适配器以及为ListView指定适配器。然后申请运行时权限。在用户授权后,调用readContacts()方法来读取系统联系人。

这里使用的是ContentResolver类的query()方法,需要传入Uri对象,这里的内容Uri即ContactsContract.CommonDatakinds.Phone类提供的CONTENT_URI常量,已经做好了封装。得到Cursor对象后,就可以开始取出联系人姓名和手机号了。最后不要忘了将Cursor对象关闭。

与此同时,还要记得在AndroidManifest.xml中加入读取系统联系人的权限。

8.4 创建自己的ContentProvider

访问其他程序的ContentProvider只需要获得该应用程序的内容URI,然后借助ContentResolver进行增删改查操作就可以了。

8.4.1 创建ContentProvider的步骤

而如果想要给其他程序提供本程序的数据,实现跨程序数据共享功能,可以通过新建一个类去继承ContentProvider来实现,ContentProvider类有6个抽象方法,需要全部重写。

内容URI的格式为:content://com.example.app.provider/table1,这就表示期望访问的的是com.example.app这个应用的table1表中的数据,除此之外,还可以加上一个id: 即content://com.example.app.provider/table1/1,这就表示期望访问的是com.example.app这个应用的table1表中的id为1的数据。以路径结尾表示期望访问表中所有的数据,以id结尾表示希望访问表中拥有相应ID的数据。这里还可以使用通配符。

这里还需要借助UriMatcher这个类来实现匹配内容Uri的功能。UriMatcher类提供了一个addURI()函数,可以将authority、path、和一个自定义代码传入进去。

8.4.2 实现跨程序数据共享

跨程序数据共享的时候不能直接使用Toast,所有需要将MyDatabaseHelper类中onCreate()中的提示去掉。然后新建一个ContentProvider,如果是Android Studio建立的,会自动在AndroidManifest.xml中注册。然后修改DataBaseProvider中的代码如下:

class DatabaseProvider : ContentProvider() { private val bookDir = 0 private val bookItem = 1 private val categoryDir = 2 private val categoryItem = 3 private val authorities = "com.example.databaseset.provider" private var dbHelper: MyDatabaseHelper? = null private val uriMatcher by lazy { val matcher = UriMatcher(UriMatcher.NO_MATCH) matcher.addURI(authorities, "book", bookDir) matcher.addURI(authorities, "book/#", bookItem) matcher.addURI(authorities, "category", categoryDir) matcher.addURI(authorities, "category/#", categoryItem) matcher } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = dbHelper?.let { val db = it.writableDatabase val deleteRows = 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 } deleteRows } ?: 0 override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) { bookDir -> "vnd.android.cursor.dir/vnd.com.example.datasettest.provider.book" bookItem -> "vnd.android.cursor.item/vnd.com.example.datasettest.provider.book" categoryDir -> "vnd.android.cursor.dir/vnd.com.example.datasettest.provider.category" categoryItem -> "vnd.android.cursor.item/vnd.com.example.datasettest.provider.category" else -> null } 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://$authorities/book/$newBookId") } categoryDir, categoryItem -> { val newCategoryId = db.insert("Category", null, values) Uri.parse("content://$authorities/category/$newCategoryId") } else -> null } uriReturn } override fun onCreate() = context?.let { dbHelper = MyDatabaseHelper(it, "BookStore.db", 2) true } ?: false override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?) = dbHelper?.let { //查询数据 val db = it.writableDatabase val cursor = when (uriMatcher.match(uri)) { bookDir -> db.query("book", projection, selection, selectionArgs, null, null, sortOrder) bookItem -> { 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?) = dbHelper?.let { val db = it.writableDatabase val updateRows = 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 } updateRows } ?: 0 }

这里初始化UriMatcher的时候用到了by lazy,这时Kotlin提供的懒加载技术,可以在此变量第一次被调用的时候才会执行,并且代码块中最后一行代码会自动赋给uriMatcher。接下来是每个抽象方法的具体实现了。如onCreate()方法,我们使用了getContext()、?.判空、?:、let函数等多个语法糖。而查询的流程为:先调用SQLiteOpenHelper类的getWriteableDatabase()方法得到一个SQLiteDatabase类的实例,然后使用when语句判断传入的URI,根据不同的URI在数据库中执行不同的查询语句。这里访问单条数据的时候调用了Uri对象的getPathSegments()方法,它会将内容URI在协议之后的部分按照/分隔,并将分隔后的结果存入一个字符串数组中,第0个位置就是路径,第1个位置就是id了。其他的增删改的操作也差不多。借助UriMatcher的 match方法判断是哪个Uri,然后创建SQLiteDatabase的实例,去增删改本地DB的数据。

最后要注意的是getType()方法,其用于获取Uri对象所对应的MIME类型,一个内容Uri对象的MIME类型有一定的格式:

以vnd开头如果内容URI以路径结尾,则后接android.cursor.dir/,如果以id结尾,则后接android.cursor.item/最后接上vnd..

可以看到,通过快捷方式创建的ContentProvider已经在AndroidManifest.xml注册了:

... ...

此时我们的DatabaseTest项目就已经拥有了跨程序数据共享的功能了,下面创建一个新项目访问这里的数据:

在布局中加入增删改查的按钮,然后在MainActivity中修改代码如下:

class MainActivity : AppCompatActivity() { private var bookId: String? = null @SuppressLint("Range") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // insert Data addData.setOnClickListener { val uri = Uri.parse("content://com.example.databaseset.provider/book") val values = contentValuesOf( "name" to "A Clash of King", "author" to "George Martin", "pages" to 1040, "price" to 22.85 ) val newUri = contentResolver.insert(uri, values) bookId = newUri?.pathSegments?.get(1) } //query data queryData.setOnClickListener { var uri = Uri.parse("content://com.example.databaseset.provider/book") val cursor = 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.d("MainActivity", "name is $name") Log.d("MainActivity", "author is $author") Log.d("MainActivity", "pages is $pages") Log.d("MainActivity", "price is $price") } close() } } // update Data updateData.setOnClickListener { bookId?.let { val uri = Uri.parse("content://com.example.databaseset.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) } } //delete data deleteData.setOnClickListener { bookId?.let { val uri = Uri.parse("content://com.example.databaseset.provider/book/$it") contentResolver.delete(uri, null, null) } } } }

可以看到,访问ContentProvider中的数据很简单,只需要提供内容URI,然后调用ContentResolver类中的增删改查方法就可以了。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有