第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)函數類似。