《Android编程权威指南》第15章,将使用隐式intent发送短消息给Crime嫌疑人。
显式 intent —— 指定要启动的 activity 类,操作系统会负责启动它。
隐式 intent —— 描述要完成的任务,操作系统就会找到合适的应用,并在其中启动相应的 activity。
一、添加按钮部件
先在 CrimeFragment 的布局中添加两个投诉用按钮。
<string name="crime_suspect_text">Choose Suspect</string>
<string name="crime_report_text">Send Crime Report</string>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
<CheckBox
.../>
<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>
二、添加嫌疑人信息至模型层
在 Crime.kt 新增成员变量。
@Entity
data class Crime(
@PrimaryKey val id: UUID = UUID.randomUUID(),
var title: String = "",
var date: Date = Date(),
var isSolved: Boolean = false,
var requiresPolice: Boolean = false,
var suspect:String = ""
)
打开 CrimeDatabase.kt,修改数据库版本,由 1 到 2。
@Database(entities = [Crime::class], version = 2, exportSchema = false)
@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 ''"
)
}
}
创建一个Migration对象更新数据库,Migration类构造函数需要两个参数,第一个是迁移前的数据库版本,第二个是迁移到的版本。
打开 CrimeRepository.kt,在创建CrimeDatabase时,把Migration添加给Room。
class CrimeRepository private constructor(context: Context) {
private val database: CrimeDatabase = Room.databaseBuilder(
context.applicationContext,
CrimeDatabase::class.java,
DATABASE_NAME
).addMigrations(migration_1_2).build()
...
}
为数据库转换提供迁移很重要。如果不提供,Room则会先删除旧版本数据库,再创建新版本数据库。这意味着数据会全部丢失,那就不太好了。
三、 使用格式化字符串
接下来就是使用带有占位符(可在应用运行时替换)的格式化字符串。
在string.xml中添加字符串资源,%1$s、%2$s等特殊字符串是占位符,它们接受字符串参数。
<string name="crime_report">%1s!The Crime was discovered on 2%s.%3s,and %4s</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>
在 CrimeFragment.kt 中,添加 getCrimeReport() 函数:
private const val DATE_FORMAT = "EEE, MMM, dd"
class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
...
private fun getCrimeReport(): String {
val solvedStr = if (mCrime.isSolved) {
getString(R.string.crime_report_solved)
} else {
getString(R.string.crime_report_unsolved)
}
val dateStr = android.text.format.DateFormat.format(DATE_FORMAT, mCrime.date).toString()
var suspect = if (mCrime.suspect.isBlank()) {
getString(R.string.crime_report_no_suspect)
} else {
getString(R.string.crime_report_suspect, mCrime.suspect)
}
return String.format(resources.getString(R.string.crime_report), mCrime.title, dateStr, solvedStr, suspect)
}
}
四、使用隐式intent
隐式 intent 的组成:
-
要执行的操作
通常以Intent类中的常量来表示。比如,URL 是 Intent.ACTION_VIEW;邮件使用 Intent.ACTION_SEND。
Intent 介绍官方地址:https://developer.android.com/reference/android/content/Intent
-
待访问数据的位置
比如网页的URL,文件的URI,或者是指向ContentProvider中某条记录的某个内容URI(content URI)。
-
操作涉及的数据类型
指MIME形式的数据类型,比如 text/html 或 audio/mpeg3。
-
可选类别
https://developer.android.com/guide/components/intents-filters?hl=zh-cn
注意,显式intent也可以使用隐式intent的操作和数据部分。这相当于要求特定的activity去做特定的事。
接下来就是使用一些隐式 intent 发送信息,在CrimeFragment中给发送的那个button添加点击事件:
mBinding.btnCrimeReport.setOnClickListener {
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, getCrimeReport())
putExtra(Intent.EXTRA_SUBJECT, getString(R.string.crime_report_subject))
}.also { intent -> startActivity(intent) }
}
消息内容和主题是作为 extra 附加到 intent 上的。注意,这些 extra 信息使用了Intent类中定义的常量。
点击效果:
创建另一个隐式intent,让用户从联系人应用里选择嫌疑人。
...
class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
...
val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val contactUri: Uri = it.data?.data!!
val queryFilds = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
val cursor = requireActivity().contentResolver.query(contactUri,queryFilds,null,
null,null)
cursor.use {
if (it?.count == 0){
return@use
}
it?.moveToFirst()
val suspect = it?.getString(0)
mCrime.suspect = suspect!!
crimeDetailViewModel.saveCrime(mCrime)
mBinding.btnCrimeSuspect.text = suspect
}
}
}
...
mBinding.btnCrimeSuspect.apply {
val pickContactIntent =
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
setOnClickListener {
startForResult.launch(pickContactIntent)
}
}
...
private fun updateUI() {
...
if (mCrime.suspect.isNotEmpty()){
mBinding.btnCrimeSuspect.text = mCrime.suspect
}
}
...
}
下面会涉及到一个知识点,是有关于 android 四大组件之一的 content provider,详情参考:https://developer.android.com/guide/topics/providers/content-provider-basics?hl=zh-cn
上述第二个隐式 intent 其实有风险会奔溃了,万一用户手机上没有联系人的应用呢。这个时候的解决办法就是,通过操作系统中的 PackageManager 类进行自检。
mBinding.btnCrimeSuspect.apply {
val pickContactIntent =
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
setOnClickListener {
startForResult.launch(pickContactIntent)
}
val packageManager: PackageManager = requireActivity().packageManager
val resolvedActivity: ResolveInfo? =
packageManager.resolveActivity(pickContactIntent, PackageManager.MATCH_DEFAULT_ONLY)
if (resolvedActivity == null) {
isEnabled = false
}
}
PackageManager 知道 Android设备上安装了哪些组件以及哪些 activity。调用resolveActivity(Intent, Int) 函数,可以找到匹配给定 Intent 任务的 activity。flag标志 MATCH_DEFAULT_ONLY 限定只搜索带 CATEGORY_DEFAULT 标志的 activity。
如果搜到目标,它会返回ResolveInfo告诉我们找到了哪个activity;如果找不到,必须禁用嫌疑人按钮,否则应用就会崩溃。
如果想验证过滤器,可以随意添加个别的类别的 intent。
五、挑战练习:又一个隐式intent
新增一个按钮,允许用户直接拨打陋习嫌疑人的电话。
这里会涉及到一个运行时的权限请求,有关的知识点参考:https://developer.android.com/training/permissions/requesting
也可以直接使用其他人封装好的权限请求库,用起来很方便。
比如说:https://github.com/guolindev/PermissionX
下次再来更新这个挑战练习!下次一定!
六、其他
CriminalIntent 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/CriminalIntent