《Android 编程权威指南》学习笔记 : 第15章 隐式 intent
第15章 隐式 intent
系列目录:https://www.cnblogs.com/easy5weikai/p/16322845.html
本节要点
- Room 的数据库迁移
- 格式化字符串资源
- 隐式 intent,其组成部分:
- 要执行的操作、数据位置、数据类型、类别(可选)
- 隐式 intent,启动操作系统内置的联系人应用程序
数据库迁移
Crime 添加新字段
代码清单:Crime
@Entity
data class Crime(
...
var suspect: String = ""
)
定义数据库迁移对象
代码清单:database/CrimeDatabase.kt
@Database(entities = [ Crime::class], version = 2, exportSchema = true)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
abstract fun crimeDao(): CrimeDao
}
val migration_1_2 = object : Migration(1,2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"Alter Table Crime Add Column suspect Text Not Null Default ''"
)
}
}
- version = 2:由1变成2
- 定义一个迁移对象 Migration(1,2) :第一个参数:旧版本号,第二个参数是:新版本号
实现接口函数:migrate(),里面是迁移的动作 - exportSchema = true:第一个版本的时候 exportSchema = false,并删除导出的Schema 文件
现在第二个版本 exportSchema = true,没有生成导出的Schema 文件。
执行迁移
代码清单:CrimeRepository.kt
class CrimeRepository private constructor(context: Context) {
private val database: CrimeDatabase = Room.databaseBuilder(
context.applicationContext,
CrimeDatabase::class.java,
DATABASE_NAME
).addMigrations(migration_1_2)
.build()
创建了Migration后,需要把它提交给数据库。在创建CrimeDatabase实例时,把Migration添加给Room
调用build()函数之前,首先调用addMigrations(...)创建数据库迁移。addMigrations(...)函数接受多个Migration对象参数,你可以把声明好的多个addMigrations(...)全部传给它。
当应用启动,Room创建数据库时,它会检查设备上现有数据库的版本。
如果检查到的版本和定义在@Database注解里的不一样,Room会找到合适的Migration以更新数据库到最新版本。
为数据库转换提供迁移很重要。如果不提供,Room则会先删除旧版本数据库,再创建新版本数据库。这意味着数据会全部丢失,用户肯定会抱怨的。
准备
代码清单:res/values/strings.xml
添加字符串资源
<resources>
...
<string name="crime_suspect_text">Choose Suspect</string>
<string name="crime_report_text">Send Crime Report</string>
<string name="crime_report">%1$s!
The crime was discovered on %2$s. %3$s, and %4$s
</string>
<string name="crime_report_solved">The case is solved</string>
<string name="crime_report_unsolved">The case is not solved</string>
<string name="crime_report_no_suspect">There is no suspect.</string>
<string name="crime_report_suspect">the suspect is %s.</string>
<string name="crime_report_subject">CriminalIntent Crime Report</string>
<string name="send_report">Send crime report via</string>
<resources>
添加两个按钮
代码清单:res/layout/fragment_crime.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<Button
android:id="@+id/crime_suspect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_suspect_text"/>
<Button
android:id="@+id/crime_report"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_report_text"/>
</LinearLayout>
发送消息
创建隐式 intent,这里使用了intent4个组成部分的3个:
- 要执行的操作:Intent(Intent.ACTION_SEND)
- 数据位置:intent.putExtra(Intent.EXTRA_TEXT, getCrimeReport()):在这个:Intent.EXTRA_TEXT 位置保存数据
- 数据类型:intent.type = "text/plan"
- 类别(可选):这里不设置
代码清单:CrimeFragment.kt
class CrimeFragment : Fragment() {
private lateinit var reportButton: Button
override fun onCreateView(...) {
reportButton = view.findViewById(R.id.crime_report) as Button
}
override fun onStart() {
super.onStart()
reportButton.setOnClickListener {
Intent(Intent.ACTION_SEND).apply {
type = "text/plan"
putExtra(Intent.EXTRA_TEXT, getCrimeReport())
putExtra(Intent.EXTRA_SUBJECT, getString(R.string.crime_report_subject))
}.also { intent ->
// startActivity(intent)
var chooserIntent =
Intent.createChooser(intent, getString(R.string.send_report))
startActivity(chooserIntent)
}
}
...
// 获取报告内容
private fun getCrimeReport(): String {
val solvedString = if (crime.isSolved) {
getString(R.string.crime_report_solved)
} else {
getString(R.string.crime_report_unsolved)
}
val dateString = DateFormat.format(DATE_FORMAT, crime.date).toString()
val suspect = if (crime.suspect.isBlank()) {
getString(R.string.crime_report_no_suspect)
} else {
getString(R.string.crime_report_suspect, crime.suspect)
}
return getString(
R.string.crime_report,
crime.title, dateString, solvedString, suspect
)
}
...
}
其中,
-
使用 chooserIntent 再包装一层 intent,避免用户选择了默认执行程序后,不再显示其它应用列表。
-
能处理 Intent.ACTION_SEND的应用程序,根据约定好的内置的数据位置(关键字),获取数据:
- Intent.EXTRA_SUBJECT:主题
- Intent.EXTRA_TEXT :文本
-
getString(R.string.crime_report_subject),根据字符串资源ID,获取字符串
真机运行效果
微信
如果选【微信】,会弹出微信界面:
选择一个联系人,点击【分享】
然后消息就发送出去了:
邮件
如果选【邮件】,会弹发送邮件界面:
对方就会收到邮件
联系人应用程序
启动联系人应用程序
代码清单:CrimeFragment.kt
private const val REQUEST_CONTACT = 1
class CrimeFragment : Fragment() {
private lateinit var suspectButton: Button
override fun onCreateView(...) {
suspectButton = view.findViewById(R.id.crime_suspect) as Button
}
override fun onStart() {
super.onStart()
...
suspectButton.apply {
var pickContactIntent =
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
setOnClickListener {
startActivityForResult(pickContactIntent, REQUEST_CONTACT)
}
}
...
private fun updateUI() {
...
if (crime.suspect.isNotEmpty()) {
suspectButton.text = crime.suspect
}
}
}
创建隐式 intent,从联系人应用程序里选嫌疑人(suspect),
- 要执行的操作:Intent(Intent.ACTION_PICK)
- 数据位置:ContactsContract.Contacts.CONTENT_URI
启动 Activit:
startActivityForResult(pickContactIntent, REQUEST_CONTACT)
该方法已经被弃用!
点击【CHOOSE SUSPECT】按钮后,弹出【联系人应用程序】
从联系人应用程序返回的数据
很多应用程序都会共享联系人,Android内置提供了一个处理联系人信息:ContentProvider类。
该类的实例封装了联系人数据库并提供给其它应用程序。我们可以通过 ContentResolver 访问 ContentProvider。
代码清单:CrimeFragment.kt
class CrimeFragment : Fragment() {
...
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when {
requestCode != Activity.RESULT_OK -> return
requestCode == REQUEST_CONTACT && data != null -> {
val contactUrl: Uri = data.data ?: return
//定义要查询哪个字段
val queryFields = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
// 执行查询
val cursor = requireActivity().contentResolver
.query(contactUrl, queryFields, null, null, null)
cursor?.use {
if (it.count == 0) {
return
}
it.moveToFirst()
val suspect = it.getString(0) //获取第一列(字段)
crime.suspect = suspect
crimeDetailViewModel.saveCrime(crime)
suspectButton.text = suspect
}
}
}
}
...
}
不幸的是:Fragment.startActivityForResult(...)方法已经被弃用了,在高版本的Android系统中,运行的CriminalIntent应用程序
点击【CHOOSE SUSPECT】按钮后,弹出【联系人应用程序】并选择联系人后,Fragment.onActivityResult()方法没有被回调。
registerForActivityResult
这里我们使用registerForActivityResult() 来替换,修改上面的代码
代码清单:CrimeFragment.kt
class CrimeFragment : Fragment() {
...
private lateinit var suspectButton: Button
private val pickContactIntent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
private lateinit var pickContactActivityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
pickContactActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK
&& result.data != null
&& result.data?.data != null
) {
val contactUrl: Uri = result.data?.data!!
//定义要查询哪个字段
val queryFields = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
// 执行查询
val cursor = requireActivity().contentResolver
.query(contactUrl, queryFields, null, null, null)
cursor?.use {
if (it.count > 0) {
it.moveToFirst()
val suspect = it.getString(0) //获取第一列(字段)
crime.suspect = suspect
crimeDetailViewModel.saveCrime(crime)
suspectButton.text = suspect
}
}
}
}
}
override fun onCreateView(...) {
...
suspectButton = view.findViewById(R.id.crime_suspect) as Button
}
override fun onStart() {
super.onStart()
...
suspectButton.apply {
setOnClickListener {
//已被弃用:startActivityForResult(pickContactIntent, REQUEST_CONTACT)
pickContactActivityResultLauncher.launch(pickContactIntent)
}
}
}
private fun updateUI() {
...
if (crime.suspect.isNotEmpty()) {
suspectButton.text = crime.suspect
}
}
}
其中:
-
pickContactIntent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI):创建隐式 intent,从联系人应用程序里选嫌疑人(suspect),
- 要执行的操作:Intent(Intent.ACTION_PICK)
- 数据位置:ContactsContract.Contacts.CONTENT_URI
-
registerForActivityResult 注册返回结果时的回调函数,并返回一个 Activity的启动器 pickContactActivityResultLauncher
-
pickContactActivityResultLauncher.launch(pickContactIntent) :使用Activity的启动器 pickContactActivityResultLauncher,传入 intent 启动 activity。
重新运行应用程序,点击【CHOOSE SUSPECT】按钮后,弹出【联系人应用程序】,
选择一个联系人:
选择完成后,自动返回到 CrimeFragment界面,同时
【CHOOSE SUSPECT】的文本被更新为刚才选择的联系人,如下图所示:
关于权限
联系人应用返回包含intent中的Uris数据给父Activit时,会添加一个 Intent.FLAG_GRANT_READ_URI_PERMISSION 标志,该标志告诉Android,CriminalIntent应用中的父Activit可以使用联系人数据一次。
这很有用,因为你不需要访问整个联系人数据库,只要访问其中的一条联系人信息就可以了。
检查可响应任务的 activity
因为有些设备上根本没有联系人应用。如果操作系统找不到匹配的activity,应用就会崩溃。
解决办法是首先通过操作系统中的PackageManager类进行自检。在onStart()函数中实现检查
代码清单:CrimeFragment.kt
override fun onStart() {
super.onStart()
...
suspectButton.apply {
setOnClickListener {
//已被弃用:startActivityForResult(pickContractIntent, REQUEST_CONTACT)
pickContactActivityResultLauncher.launch(pickContactIntent)
}
// 检查可响应任务的 activity,避免程序崩溃
val packageManager: PackageManager = requireActivity().packageManager
val resolvedActivity: ResolveInfo? =
packageManager.resolveActivity(pickContactIntent,
PackageManager.MATCH_DEFAULT_ONLY)
if (resolvedActivity == null) {
isEnabled = false
}
}
}
Android设备上安装了哪些组件以及包括哪些activity,PackageManager全都知道。
调用resolveActivity(Intent, Int)函数,可以找到匹配给定Intent任务的activity。
flag标志MATCH_DEFAULT_ONLY限定只搜索带CATEGORY_DEFAULT标志的activity。这和startActivity(Intent)函数类似。