《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)函数类似。

posted @ 2022-06-02 14:18  easy5  阅读(89)  评论(0编辑  收藏  举报