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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章