Activity 相关
- Activity: 负责用户界面的展示和用户交互,学习 Activity 就要学习 Fragment,虽然它不是四大组件之一,但是它在我们的开发工作中也是频频被使用到,且必须和 Activity 一块使用,常用于分模块开发,比如慕课首页的几个 tab,每个 tab 都是对应着一个 Fragment
1.Activity 生命周期
方法 | 说明 |
---|---|
onCreate() | 该方法会在 Activity 第一次创建时进行调用,在这个方法中通常会做 Activity 初始化相关的操作,例如:加载布局、绑定事件等 |
onStart() | 这个方法会在 Activity 由不可见变为可见的时候调用,但是还不能和用户进行交互。 |
onResume() | 表示 Activity 已经启动完成,进入到了前台,可以同用户进行交互了。 |
onPause() | 这个方法。可以在这里释放系统资源,动画的停止,不宜在此做耗时操作。 发生场景 1.在系统准备去启动另一个 Activity 的时候调用 2.按下返回键 3.从前台切换到后台 |
onStop() | 当 Activity 不可见的时候回调此方法。需要在这里释放全部用户使用不到的资源。可以做较重量级的工作,如对注册广播的解注册,对一些状态数据的存储。此时 Activity 还不会被销毁掉,而是保持在内存中,但随时都会被回收。通常发生在启动另一个 Activity 或切换到后台时 发生场景和 onPause()相同 |
onDestroy() | Activity 即将被销毁。此时必须主动释放掉所有占用的资源。 |
onReStart() | 这个方法在 Activity 由停止状态变为运行状态之前调用,也就是 Activity 被重新启动了(APP 切到后台会进入 onStop(), 再切换到前台时会触发 onRestart()方法) |
- 按 home 键 : 运行中状态 转变 开始 执行 onPause() onStop()
- 返回键: onPause() —> onResume()
- 回到这个 Activity : 从 onStop() 转变 开始执行 onRestart() —> onStart() —>onResume()
2.Activity 组件创建
class SecondActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
- 选择复写只有一个参数的 onCreate 方法
3.Activity 组件注册
四大组件需要在 AndroidManifest 文件中配置否则无法使用,类似 Activity 无法启动,
在 AndroidManifest.xml 中注册
<activity android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<!--自定义 action时一般包名.action.全大写类名-->
<action android:name="com.example.componentLearn.action.SECONDACTIVITY"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!--
android:name是对应Activity的类名称
android:exported 是否支持其它应用调用当前组件。默认值:如果包含有intent-filter 默认值为true; 没有intent-filter默认值为false
android:label是Activity标题栏显示的内容. 现已不推荐使用
intent-filter 是意图过滤器. 常用语隐式跳转
action android:name 是动作名称,是指intent要执行的动作
category android:name 是过滤器的类别, 一般情况下,每个 中都要显示指定一个默认的类别名称,即<category android:name="android.intent.category.DEFAULT" />
-->
- 但是上面的代码中没有指定默认类别名称,这是一个例外情况,因为其 中的是"android.intent.action.MAIN",意思是这个 Activity 是应用程序的入口点,这种情况下可以不加默认类别名称。
4.Activity 启动与参数传递
tips 在 Android 中我们可以通过下面两种方式来启动一个新的 Activity,注意这里是怎么启动,分为显示启动和隐式启动!
4.1 显式启动:通过包名来启动
无参数跳转
textView.setOnClickListener {
//MainActivity@this : Context
val intent = Intent(MainActivity@this , SecondActivity::class.java)
startActivity(intent)
}
- 日志 ActivityA 跳转到 ActivityB
2021-11-03 22:08:59.781 4562-4562/com.example.componentlearn E/MainActivity:: onCreate
2021-11-03 22:08:59.790 4562-4562/com.example.componentlearn E/MainActivity:: onStart
2021-11-03 22:08:59.797 4562-4562/com.example.componentlearn E/MainActivity:: onResume
2021-11-03 22:09:07.295 4562-4562/com.example.componentlearn E/MainActivity:: onPause
2021-11-03 22:09:07.332 4562-4562/com.example.componentlearn E/SecondActivity:: onCreate
2021-11-03 22:09:07.337 4562-4562/com.example.componentlearn E/SecondActivity:: onStart
2021-11-03 22:09:07.338 4562-4562/com.example.componentlearn E/SecondActivity:: onResume
2021-11-03 22:09:08.009 4562-4562/com.example.componentlearn E/MainActivity:: onStop
有参数跳转
textView.setOnClickListener {
//有参跳转
val intent = Intent(MainActivity@this , SecondActivity::class.java)
intent.putExtra("extra_data","extra_data")
intent.putExtra("extra_int_data",100)
startActivity(intent)
}
- ActivityB 接受参数
val stringExtra = intent.getStringExtra("extra_data")
val intExtra = intent.getIntExtra("extra_int_data" , 0 )
期待从目标页获取数据
registerForActivityResult---->比如启动相册获取图片
lateinit 延迟初始化
①假设从A--->B页面,以registerForActivityResult方式启动
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
){result ->
val data = result.data
val resultCode = result.resultCode
val stringExtraResult = data?.getStringExtra("result_extra_string")
val intExtraResult = data?.getIntExtra("result_extra_int",0)
textView.text = "MainActivity result [${resultCode}] ${stringExtraResult} ---- ${intExtraResult}"
}.launch(Intent(this,SecondActivity::class.java))
②如果B页面返回时,调用了
textView.setOnClickListener {
val resultIntent = Intent()
resultIntent.putExtra("result_extra_string" , "result_extra_string")
resultIntent.putExtra("result_extra_int" , 1000)
setResult(Activity.RESULT_OK , resultIntent)
finish() //关闭SecondActivity 等同于点击返回键
}
tips startActivityForResult 过时用 registerForActivityResult 代替 https://www.zbug.cc/index.php/archives/registerForActivityResult.html
- 这个事件中两个 Activity 的生命周期
2021-11-04 11:19:12.221 4381-4381/com.example.componentlearn E/MainActivity:: onCreate
2021-11-04 11:19:12.233 4381-4381/com.example.componentlearn E/MainActivity:: onStart
2021-11-04 11:19:12.235 4381-4381/com.example.componentlearn E/MainActivity:: onResume
2021-11-04 11:19:12.267 4381-4381/com.example.componentlearn E/MainActivity:: onPause
2021-11-04 11:19:12.705 4381-4381/com.example.componentlearn E/SecondActivity:: onCreate
2021-11-04 11:19:12.709 4381-4381/com.example.componentlearn E/SecondActivity:: onStart
2021-11-04 11:19:12.709 4381-4381/com.example.componentlearn E/SecondActivity:: onResume
2021-11-04 11:19:12.883 4381-4381/com.example.componentlearn E/MainActivity:: onStop
2021-11-04 11:19:20.553 4381-4381/com.example.componentlearn E/SecondActivity:: onPause
2021-11-04 11:19:20.565 4381-4381/com.example.componentlearn E/MainActivity:: onRestart
2021-11-04 11:19:20.568 4381-4381/com.example.componentlearn E/MainActivity:: onStart
2021-11-04 11:19:20.569 4381-4381/com.example.componentlearn E/MainActivity:: onResume
2021-11-04 11:19:21.141 4381-4381/com.example.componentlearn E/SecondActivity:: onStop
2021-11-04 11:19:21.142 4381-4381/com.example.componentlearn E/SecondActivity:: onDestroy
4.2. 隐式启动
- 通过指定 action 和 category 的信息,让系统去分析这个 Intent,并找出合适的 Activity 去启动。
<activity android:name=".SecondActivity"
android:exported="true">
<intent-filter>
<!--自定义 action时一般包名.action.全大写类名-->
<action android:name="com.example.componentLearn.action.SECONDACTIVITY"/>
<category android:name="com.example.componentLearn.category.SecondActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
textView.setOnClickListener {
val intent = Intent()
intent.action = "com.example.componentLearn.action.SECONDACTIVITY"
intent.addCategory("com.example.componentLearn.category.SECONDACTIVITY")
intent.putExtra("extra_data" , "extra_data")
intent.putExtra("extra_data_int" , 111)
//隐式启动
startActivity(intent)
}
5.原生常见的 Activtiy
拨打电话
val uri: Uri = Uri.parse("tel: 11111")
val intent = Intent(Intent.ACTION_DIAL,uri)
startActivity(intent)
发送短信
val uri:Uri = Uri.parse("smsto:1111")
val intent = Intent(Intent.ACTION_SENDTO , uri)
intent.putExtra("sms_body" , "发送短信")
startActivity(intent)
打开网页
val uri:Uri = Uri.parse("https://www.baidu.com")
val intent = Intent(Intent.ACTION_VIEW , uri)
startActivity(intent)
打开相册
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
startActivity(intent)
多媒体播放
val uri:Uri = Uri.parse("file:///sdcard/1.mp3")
val intent = Intent(Intent.ACTION_VIEW , uri)
intent.type = "audio/mp3"
startActivity(intent)
打开摄像头拍照
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, 0);
>>> 在Activity的onActivityResult方法回调中取出照片数据
Bundle extras = intent.getExtras();
Bitmap bitmap = (Bitmap) extras.get("data");
从图库选图并剪切
// 获取并剪切图片
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra("crop", "true"); // 开启剪切
intent.putExtra("aspectX", 1); // 剪切的宽高比为1:2
intent.putExtra("aspectY", 2);
intent.putExtra("outputX", 20); // 保存图片的宽和高
intent.putExtra("outputY", 40);
intent.putExtra("output", Uri.fromFile(new File("/mnt/sdcard/temp"))); // 保存路径
intent.putExtra("outputFormat", "JPEG");// 返回格式
startActivityForResult(intent, 0);
>>>> 在Activity的onActivityResult方法中去读取保存的文件
剪切指定图片文件
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setClassName("com.android.camera", "com.android.camera.CropImage");
intent.setData(Uri.fromFile(new File("/mnt/sdcard/temp")));
intent.putExtra("outputX", 1); // 剪切的宽高比为 1:2
intent.putExtra("outputY", 2);
intent.putExtra("aspectX", 20); // 保存图片的宽和高
intent.putExtra("aspectY", 40);
intent.putExtra("scale", true);
intent.putExtra("noFaceDetection", true);
intent.putExtra("output", Uri.parse("file:///mnt/sdcard/temp"));
startActivityForResult(intent, 0);
>>>> 在 Activity 的 onActivityResult 方法中去读取保存的文件
进入手机的无线网络设置页面
// 进入无线网络设置界面(其它可以举一反三)
Intent intent = new Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS);
startActivityForResult(intent, 0);
6. Activity 四种启动模式
standard
- 标准模式
- 默认值,多实例模式。每启动一次,都会创建一个新的 Activity 实例。
- 启动的生命周期为:onCreate()->onStart()->onResume()
singleTop
栈顶复用模式
如果任务栈顶已经存在需要启动的目标 Activity,则直接启动,并会回调 onNewIntent()方法,生命周期顺序为: onPause() ->onNewIntent()->onResume()
如果任务栈上顶没有需要启动的目标 Activity,则创建新的实例,此时生命周期顺序为: onCreate()->onStart()->onResume()
两种情况如下图,从图中可以看出,此模式下还是会出现多实例,只要启动的目标 Activity 不在栈顶的话
singleTask
栈内复用模式
一个任务栈只能有一个实例
有几种情况:
当启动的 Activity 目标任务栈不存在时,则以此启动 Activity 为根 Activity 创建目标任务栈,并切换到前面
D 为 singleTask 模式
当启动的 Activity 存在时,则会直接切换到 Activity 所在的任务栈,并且任务栈中在 Activity 上面的所有其他 Activity 都出栈(调用 destroy()),此时启动的 Activity 位于任务栈顶,并且会回调 onNewIntent()方法
singleInstance
- singleInstance 名称是单例模式,即 App 运行时,该 Activity 只有一个实例。既然只有一个,那么也就说明很重要、很特殊,我们需要将其“保护起来”。单例模式的“保护措施”是将其单独放到一个任务栈中
Fragment
- 注意事项: Fragment 并不能单独使用,他需要嵌套在 Activity 中使用,尽管他拥有自己的生命周期,但是还是会受到宿主 Activity 的生命周期的影响,比如 Activity 被 destory 销毁了,他也会跟着销毁!一个 Activity 可以嵌套多个 Fragment
1.Fragment 的生命周期
四个场景用于加深对 Fragment 生命周期的理解
①Activity 加载 Fragment 的时候,依次调用下面的方法: onAttach -> onCreate -> onCreateView -> onActivityCreated -> onStart ->onResume
② 当我们启动一个新的页面, 此时 Fragment 所在的 Activity 不可见,会执行 onPause
③ 当新页面返回后,当前 Activity 和 Fragment 又可见了,会再次执行 onStart 和 onResume
④ 退出了 Activity 的话,那么 Fragment 将会被完全结束, Fragment 会进入销毁状态 onPause -> onStop -> onDestoryView -> onDestory -> onDetach
onActivityCreated 已过时 常用 onCreateView 当前 fragment 所对应的视图对象 onResume 恢复动画/视频播放
2. Fragment 的动态添加与数据传递
2.1 动态添加 Fragment
- ① 创建 Fragment
class SecondActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val fragment = SecondFragment()
//事务操作对象
val ft:FragmentTransaction = supportFragmentManager.beginTransaction()
ft.add(R.id.container , fragment)
ft.commitAllowingStateLoss()
}
}
2.2 Fragment 常见的操作
val fragment = StudyFragment()
val ft = supportFragmentManager.beginTransaction()
if(!fragment.isAdded()){
ft.add(R.id.container,fragment) //把fragment添加到事务中,当且仅当该fragment未被添加过
}
ft.show(fragment) //显示出fragment的视图
ft.hide(fragment) //隐藏fragment,使得它的视图不可见
ft.remove(fragment)//移除fragment
//替换fragment,之前添加过的fragment都会被暂时移除,把当前这个fragment添加到事务中
ft.replace(R.id.container,fragment)
//提交事务,执行对fragment的add、replace、show、hide操作
ft.commitAllowingStateLoss()
2.3 给 Fragment 传递数据
- Activity 传入数值
class SecondActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
val fragment = SecondFragment()
//给Fragment传递数据
val bundle = Bundle()
bundle.putInt("int_extra" , 100)
bundle.putString("string_extra" , "string_extra")
fragment.arguments = bundle
//事务操作对象
val ft:FragmentTransaction = supportFragmentManager.beginTransaction()
ft.add(R.id.container , fragment)
ft.commitAllowingStateLoss()
}
}
- Fragment 接受数据
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val intValue = arguments?.getInt("int_extra")
val strValue = arguments?.getString("string_extra")
//view是onCreateView return的view
val textview = view as TextView
textview.text = "${intValue} ---${strValue}"
}
思考:运行时 Fragment 如何获取 Activity 中的数据、又如何将数据传递到 Activity 中呢? https://developer.android.com/guide/fragments/communicate?hl=zh-cn
设计并实现底部导航栏页面结构
Activity 页面布局
- 此布局和如果 创建项目选择带导航栏 的样式相同
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggle_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:selectionRequired="false"
android:background="#08000000"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/tab1"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@android:color/transparent"
android:text="Tab1"
android:textColor="#000000"
android:textSize="12sp"
app:icon="@drawable/ic_home_black_24dp"
app:iconGravity="textTop"
app:iconTint="@color/black"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/tab2"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@android:color/transparent"
android:text="Tab2"
android:textColor="#000000"
android:textSize="12sp"
app:icon="@drawable/ic_notifications_black_24dp"
app:iconGravity="textTop"
app:iconTint="@color/black"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/tab3"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="@android:color/transparent"
android:text="Tab3"
android:textColor="#000000"
android:textSize="12sp"
app:icon="@drawable/ic_dashboard_black_24dp"
app:iconGravity="textTop"
app:iconTint="@color/black"
tools:ignore="HardcodedText" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
- tips
- 占用父布局(除了导航栏)剩余的空间
android:layout_height="0dp"
android:layout_weight="1"
2.水平平分
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
监听选中事件
- 在 onCreate 方法中
//改变选中按钮的颜色
binding.toggleGroup.addOnButtonCheckedListener { group:MaterialButtonToggleGroup, checkedId:Int , isChecked:Boolean ->
val childCount = group.childCount
var selectIndex = 0
for (index in 0 until childCount){
val button = group.getChildAt(index) as MaterialButton
if(button.id == checkedId){
//选中的按钮变红
selectIndex = index
button.setTextColor(Color.CYAN)
button.iconTint = ColorStateList.valueOf(Color.CYAN)
} else {
button.setTextColor(Color.BLACK)
button.iconTint = ColorStateList.valueOf(Color.BLACK)
}
}
switchFragment(selectIndex)
}
binding.toggleGroup.check(binding.tab1.id)
导航栏切换动态切换显示的 fragment
private var tab1Fragment:SecondFragment?=null
private var tab2Fragment:SecondFragment?=null
private var tab3Fragment:SecondFragment?=null
private var shownFragment:Fragment ?= null //当前正在显示的Fragment
//切换Fragment
private fun switchFragment(selectIndex: Int) {
//根据选中tab的id返回fragment
val fragment = when(selectIndex){
0->{
if(tab1Fragment == null){
tab1Fragment = SecondFragment()
val bundle = Bundle()
bundle.putString("tab" , "tab1")
tab1Fragment!!.arguments = bundle
}
tab1Fragment
}1->{
if(tab2Fragment == null){
tab2Fragment = SecondFragment()
val bundle = Bundle()
bundle.putString("tab" , "tab2")
tab2Fragment!!.arguments = bundle
}
tab2Fragment
}2->{
if(tab3Fragment == null){
tab3Fragment = SecondFragment()
val bundle = Bundle()
bundle.putString("tab" , "tab3")
tab3Fragment!!.arguments = bundle
}
tab3Fragment
}else -> {
throw IllegalStateException("下标不符合预期")
}
}?:return
val ft = supportFragmentManager.beginTransaction()
if(!fragment.isAdded){
ft.add(R.id.container , fragment)
}
ft.show(fragment)
//避免出现多个fragment重叠
if(shownFragment !=null){
ft.hide(shownFragment!!)
}
shownFragment = fragment
ft.commitAllowingStateLoss()
}
本例中多个 fragment 的生命周期
- 第一次点击各个 tab 每个 fragment onAttach----> onResume
2021-11-11 16:34:22.661 5666-5666/com.example.componentlearn E/SecondFragment: onAttach
2021-11-11 16:34:22.662 5666-5666/com.example.componentlearn E/SecondFragment: onCreate
2021-11-11 16:34:22.665 5666-5666/com.example.componentlearn E/SecondFragment: onCreateView
2021-11-11 16:34:22.675 5666-5666/com.example.componentlearn E/SecondFragment: onStart
2021-11-11 16:34:22.679 5666-5666/com.example.componentlearn E/SecondFragment: onResume
2021-11-11 16:34:24.570 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab1-- true
2021-11-11 16:34:24.573 5666-5666/com.example.componentlearn E/SecondFragment: onAttach
2021-11-11 16:34:24.574 5666-5666/com.example.componentlearn E/SecondFragment: onCreate
2021-11-11 16:34:24.575 5666-5666/com.example.componentlearn E/SecondFragment: onCreateView
2021-11-11 16:34:24.576 5666-5666/com.example.componentlearn E/SecondFragment: onStart
2021-11-11 16:34:24.577 5666-5666/com.example.componentlearn E/SecondFragment: onResume
2021-11-11 16:34:25.737 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab2-- true
2021-11-11 16:34:25.739 5666-5666/com.example.componentlearn E/SecondFragment: onAttach
2021-11-11 16:34:25.740 5666-5666/com.example.componentlearn E/SecondFragment: onCreate
2021-11-11 16:34:25.741 5666-5666/com.example.componentlearn E/SecondFragment: onCreateView
2021-11-11 16:34:25.741 5666-5666/com.example.componentlearn E/SecondFragment: onStart
2021-11-11 16:34:25.745 5666-5666/com.example.componentlearn E/SecondFragment: onResume
- tab3 切换 tab2
2021-11-11 16:35:53.583 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab3-- true
2021-11-11 16:35:53.584 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab2-- false
- tab2 切换 tab1
2021-11-11 16:35:55.533 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab2-- true
2021-11-11 16:35:55.533 5666-5666/com.example.componentlearn E/SecondFragment: onHiddenChanged : tab1-- false
- 当且仅当 activity 存在多个 fragment , 并且我们调用了 show - hide
- fragment 的生命周期会调用
override fun onHiddenChanged(hidden: Boolean)
- 不可见为 true , 可见为 false
service
- Service 服务不需要和用户交互,且需要长期运行任务的解决方案。负责后台任务,比如播放音乐,socket 长连接 Service 启动后默认是运行在主线程中,在执行具体耗时任务过程中要手动开启子线程,应用程序进程被杀死,所有依赖该进程的 Service 服务也会停止运行。
1.Service 启动方式与生命周期
- Service 启动方式分为两种,普通启动 startService 、绑定启动 bindService
1.1 普通启动 startService()
- 一般用于创建一个长时间持续运行的后台任务的时候才会使用,比如说 socket,文件上传下载服务
普通启动 startService(),它的生命周期和应用程序的生命周期一样长,只要应用程序不被杀死,服务就会一直运行 ,除非我们使用 stopService()
① 首次启动会创建一个 Service 实例,依次调用 onCreate()和 onStartCommand()方法,此时 Service 进入运行状态
② 如果再次调用 StartService 启动 Service,将不会再创建新的 Service 对象, 系统会直接复用前面创建的 Service 对象,调用它的 onStartCommand()方法!
③ 这样的 Service 与它的调用者无必然的联系,就是说当调用者结束了自己的生命周期, 但是只要不调用 stopService,那么 Service 还是会继续运行的!
④ 无论启动了多少次 Service,只需调用一次 StopService 即可停掉 Service
定义 Service 服务
class TestService1 : Service(){
private var TAG = "TestService1"
override fun onCreate() {
super.onCreate()
Log.e(TAG,"onCreate")
}
override fun onBind(p0: Intent?): IBinder? {
Log.e(TAG,"onBind")
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.e(TAG,"onStartCommand")
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
Log.e(TAG,"onDestroy")
super.onDestroy()
}
}
- AndroidManifest.xml 完成 Service 注册
<application>
<service android:name=".TestService1"/>
</application>
在 Avtivity 中 StartService 启动服务 (要在 AndroidManifest 注册)
private lateinit var binding : ActivityTestServiceBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTestServiceBinding.inflate(layoutInflater)
setContentView(binding.root)
//启动服务
binding.startService.setOnClickListener {
val intent = Intent(this , TestService1::class.java)
startService(intent)
}
//停止服务
binding.stopService.setOnClickListener {
val intent = Intent(this , TestService1::class.java)
stopService(intent)
}
}
日志输出与结果分析
- 对于使用 startService 的方式而言,onStartCommand 就是我们用于做后台任务的地方,如果我们多次点击 startService 按钮,会直接调 onStartCommand,而不再回调 onCreate
//第一次点击startService
2021-11-11 22:37:16.274 6417-6417/com.example.componentlearn E/TestService1: onCreate
2021-11-11 22:37:16.275 6417-6417/com.example.componentlearn E/TestService1: onStartCommand
//第二次点击startService
2021-11-11 22:37:20.167 6417-6417/com.example.componentlearn E/TestService1: onStartCommand
//第三次点击startService
2021-11-11 22:37:21.579 6417-6417/com.example.componentlearn E/TestService1: onStartCommand
//点击stopSevice
2021-11-11 22:37:22.612 6417-6417/com.example.componentlearn E/TestService1: onDestroy
//第四次点击startService
2021-11-11 22:37:24.957 6417-6417/com.example.componentlearn E/TestService1: onCreate
2021-11-11 22:37:24.959 6417-6417/com.example.componentlearn E/TestService1: onStartCommand
1.2 绑定启动 bindService()
运行一些和 Activity 生命周期相等的后台任务,如跨进程的通信
① 当首次使用 bindService()启动一个 Service 时,系统会实例化一个 Service 实例,并调用其**onCreate()和 onBind()**方法,然后调用者就可以通过返回的 IBinder 对象和 Service 进行交互了,此后如果我们再次使用 bindService 绑定 Service,系统不会创建新的 Sevice 实例,也不会再调用 onBind()方法,只会直接把 IBinder 对象返回给调用方
② 如果我们解除与服务的绑定,只需调用 unbindService(),此时 onUnbind 和 onDestory 方法将会被调用
③bindService 启动的 Service 服务是与调用者(Activity)相互关联的,可以理解为 “一条绳子上的蚂蚱”,要死一起死,在 bindService 后,一旦调用者(Activity)销毁,那么 Service 也立即终止
定义 Service 服务
class TestService2 : Service(){
private var TAG = "TestService2"
private var count = 0
private var quit = false //标记位 线程退出
override fun onCreate() {
super.onCreate()
Log.e(TAG,"onCreate")
//线程开启
Thread(Runnable {
while(true){
if(quit)
break
Thread.sleep(1000)
count++
}
}).start()
}
private val binder = MyBinder()
//内部类 继承Binder类 ,Binder类实现IBinder
inner class MyBinder : Binder(){
fun getCount() : Int{
return count
}
}
override fun onBind(p0: Intent?): IBinder? {
Log.e(TAG,"onBind")
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
Log.e(TAG,"onUnbind")
quit = true
return super.onUnbind(intent)
}
override fun onDestroy() {
Log.e(TAG,"onDestroy")
super.onDestroy()
}
}
AndroidManifest.xml 中注册服务
<application>
<service android:name=".TestService2"/>
</application>
在 Activity 中 bindService 启动服务
class TestServiceActivity : Activity(){
private lateinit var binding : ActivityTestServiceBinding
private var connection: ServiceConnection?=null
private var myBinder: TestService2.MyBinder? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTestServiceBinding.inflate(layoutInflater)
setContentView(binding.root)
connection = object:ServiceConnection{
//Activity与Service连接成功时回调该方法
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.e("TestService2","--------------Service Connected--------------")
myBinder = service as TestService2.MyBinder
}
override fun onServiceDisconnected(p0: ComponentName?) {
//Activity与Service断开连接时回调该方法
Log.e("TestService2","--------------Service Disconnected--------------")
}
}
//绑定service
val intent = Intent(this , TestService2::class.java)
bindService(intent , connection!! , Context.BIND_AUTO_CREATE)
//启动服务
binding.startService.setOnClickListener {
/* val intent = Intent(this , TestService1::class.java)
startService(intent)*/
Log.e("TestService2","*********** getCount:=${myBinder?.getCount()} ***********")
}
//停止服务
binding.stopService.setOnClickListener {
//普通启动starService()
/* val intent = Intent(this , TestService1::class.java)
stopService(intent)*/
//绑定启动bindService()
unbindService(connection!!)
}
}
//否则Activity会发生内存泄露
override fun onDestroy() {
super.onDestroy()
unbindService(connection!!)
}
}
日志输出与结果分析
- 可以在 onCreate(),onBind() 开启耗时任务如线程
2021-11-13 11:25:33.264 5247-5247/com.example.componentlearn E/TestService2: onCreate
2021-11-13 11:25:33.266 5247-5247/com.example.componentlearn E/TestService2: onBind
2021-11-13 11:25:34.069 5247-5247/com.example.componentlearn E/TestService2: --------------Service Connected--------------
2021-11-13 11:25:34.976 5247-5247/com.example.componentlearn E/TestService2: *********** getCount:=1 ***********
2021-11-13 11:25:36.415 5247-5247/com.example.componentlearn E/TestService2: *********** getCount:=3 ***********
2021-11-13 11:25:37.134 5247-5247/com.example.componentlearn E/TestService2: *********** getCount:=3 ***********
2021-11-13 11:25:38.562 5247-5247/com.example.componentlearn E/TestService2: *********** getCount:=5 ***********
2021-11-13 11:25:43.267 5247-5247/com.example.componentlearn E/TestService2: onUnbind
2021-11-13 11:25:43.267 5247-5247/com.example.componentlearn E/TestService2: onDestroy
- 使用 BindService 绑定 Service,依次调用 onCreate(),onBind()方法, 我们可以在 onBind()方法中返回自定义的 IBinder 对象;再接着调用的是 ServiceConnection 的 onServiceConnected()方法该方法中可以获得 IBinder 对象,从而进行相关操作;当 Service 解除绑定后会自动调用 onUnbind 和 onDestroyed 方法,当然绑定多客户端情况需要解除所有 的绑定才会调用 onDestoryed 方法进行销毁哦
2.Android 8.0 及以上不允许后台启动 Service 服务
Android 8.0 还对特定函数做出了以下变更:
1.如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException
2.新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数
软件不可见后台超过 60s 不可启动 service
2021-11-14 14:24:34.213 3846-3846/com.example.componentlearn E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.componentlearn, PID: 3846
java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.example.componentlearn/.TestService2 }: app is in background uid UidRecord{89692e9 u0a121 LAST bg:+1m7s709ms idle change:cached procs:1 seq(0,0,0)}
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1715)
AndroidManifest.xml 声明权限
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
服务启动兼容写法
- TestServiceActivity 的 onCreate()
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//安卓版本高于8.0 26或者Build.VERSION_CODES.O
startForegroundService(intent)
}else{
startService(intent)
}
- TestService2 中的 onCreate
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notification = Notification.Builder(applicationContext, "channel_id").build()
startForeground(1,notification)
}
BroadcastReciver
BroadcastReceiver 广播接收者: 负责页面间通信(如两个 acticity 之间),系统和 APP 通信,APP 和 APP 通信,比如监听网络连接状态变化,就是通过 BroadcastReceiver 广播接收者来实现的
Android 中, 系统自己在很多时候都会发送广播,比如电量变化,wifi 连接变化,插入耳机,输入法改变等,系统都会发送广播,这个叫系统广播。此时系统就是广播发送者
APP 想要收到这些广播,这个时候我们只需要注册一个 BroadcastReceiver,当 wifi 连接发生变化,我们注册的广播就会收到通知~。此时我们的 APP 就是广播接收者
我们也可以自己发广播,比如:登录成功后发出广播,监听这个广播的接收者就可以做些刷新页面的动作。此时我们的 APP 既是广播发送者,也是广播接收者
1.两种广播类型
- 标准广播:发出广播后,该广播事件的接收者,几乎会在同一时刻收到通知,都可以响应或不响应该事件
- 有序广播:发出广播后,同一时刻,只有一个广播接收者能收到、一个接收者处理完后之后,可以选择继续向下传递给其它接收者,也可以拦截掉广播。[不常用、不推荐使用了 5.0 以下]
2.监听系统网络连接变化
2.1 定义一个广播接收者
class TestBroadcastReceiver : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
//intent事件类型
val action = intent?.action?:return //如果为空则不处理
if(action == ConnectivityManager.CONNECTIVITY_ACTION){
val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val info = connectivityManager.activeNetworkInfo //获取网络连接类型
if(info!=null && info.isAvailable){
//有网络连接的
val typeName =info.typeName
Toast.makeText(context!! , "当前网络连接类型${typeName}" , Toast.LENGTH_LONG).show()
}else{
Toast.makeText(context!! , "当前无网络连接" , Toast.LENGTH_LONG).show()
}
}
}
}
2.2 运行时动态注册广播接收事件
class TestBroadcastReceiverActivity : AppCompatActivity(){
private lateinit var receiver: TestBroadcastReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
receiver = TestBroadcastReceiver()
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(receiver , intentFilter)
}
override fun onDestroy() {
super.onDestroy()
// 必须要在onDestroy时反注册,否则会内存泄漏
unregisterReceiver(receiver)
}
}
不要忘记在 AndroidManifest.xml 中注册 TestBroadcastReceiverActivity 不要在收到广播后进行任何耗时操作,因为在广播中是不允许开辟线程的(默认主线程), 当 onReceiver( )方法运行较长时间(超过 10 秒)还没有结束的话,那么程序会报错(ANR), 广播更多的时候扮演的是一个打开其他组件的角色,比如启动 Service,Notification 提示, Activity 等!
2.3 静态注册广播
<receiver android:name=".components.TestBroadcastReceiver">
<intent-filter >
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
从 android 8.0(API26)开始,对清单文件中静态注册广播接收者增加了限制,建议大家不要在清单文件中静态注册广播接收者,可以静态注册的是 自定义的广播事件或者一些系统级别的广播如:是否开机、电池电量
解决静态注册广播接收者收不到事件的问题
- 虽然从 Android8.0 开始,系统明确限制了静态广播注册去监听系统行为,但是应用自己定义的的广播事件还是可以使用静态注册的
AndroidManifest.xml 中注册
<!--静态注册广播-->
<receiver android:name=".TestBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.example.componentLearn.TEST_BROADCAST_RECEVIER" />
</intent-filter>
</receiver>
- TestBroadcastReceiverActivity 的 onCreate
val intent = Intent()
intent.action = "com.example.componentLearn.TEST_BROADCAST_RECEIVER"
// 下面这一行在Android 7.0及以下版本不是必须的,但是Android 8.0或者更高版本,发送广播的条件更加严苛,必须添加这一行内容。
// 创建的ComponentName实例化对象有两个参数,第1个参数是指接收广播类的包名,第2个参数是指接收 广播类 的完整类名。
intent.component = ComponentName(packageName,"com.example.componentlearn.TestBroadcastReceiver")
sendBroadcast(intent)
3.发送自定义事件广播
3.1 全局发送广播
全局发送广播,如果别人家 App 也注册了该事件监听,也能收到(信息泄露),比较不合理。
sendBroadcast(new Intent("com.example.firstapp.component.TEST_BROADCAST_RECEVIER"));
3.2 应用内发送广播
- 1.App 应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个 App
- 2.相比于全局广播(普通广播),App 应用内广播优势体现在:安全性高 & 效率高
// 使用LocalBroadcastManager来注册应用内广播
LocalBroadcastManager.getInstance(this).registerReceiver(myReceiver, itFilter)
// 使用LocalBroadcastManager来发送应用内广播
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
4.系统广播
- Android 中内置了多个系统广播:只要涉及到手机的基本操作(如开机、网络状态变化、拍照等等),都会发出相应的广播
- 每个广播都有特定的 Intent - Filter(包括具体的 action),Android 系统广播 action 如下
操作系统 | action |
---|---|
开机, 系统启动完成后(仅广播一次) | android.intent.action.BOOT_COMPLETED |
网络状态变化 | android.net.conn.CONNECTIVITY_CHANGE |
关闭或打开飞行模式 | Intent.ACTION_AIRPLANE_MODE_CHANGED |
充电时或电量发生变化 | Intent.ACTION_BATTERY_CHANGED |
拍照 | Intent.ACTION_CAMERA_BUTTON |
电池电量充足(即从电量低变化到饱满时会发出广播 | intent.action.BATTERY_OKAY |
电池电量低(即从电量充足变化到低时会发出广播 | intent.action.BATTERY_LOW |
屏幕锁屏 | Intent.ACTION_CLOSE_SYSTEM_DIALOGS |
日期改变 | Intent.ACTION_DATE_CHANGED |
时间改变 | Intent.ACTION_TIME_CHANGED |
时区改变 | Intent.ACTION_TIMEZONE_CHANGED |
电话状态改变 | TelephonyManager.ACTION_PHONE_STATE_CHANGED |
插入耳机时 | Intent.ACTION_HEADSET_PLUG |
成功安装 APK | Intent.ACTION_PACKAGE_ADDED |
卸载 APK | Intent.ACTION_PACKAGE_REMOVED |
语言改变 | Intent.ACTION_LOCALE_CHANGED |
ContentProvider
- ContentProvider 内容提供者: 负责数据存取,常用于 APP 进数据共享,跨进程数据存取等…比如读取相册,读取联系人,都是 ContentProvider 来实现的
**1.**我们想在自己的应用中访问别的应用,或者说一些 ContentProvider 暴露给我们的一些数据, 比如手机联系>人,短信、相册等!我们想对这些数据进行读取或者修改,这就需要用到 ContentProvider 了! **2.**我们自己的应用,想把自己的一些数据暴露出来,给其他的应用进行读取或操作,我们也可以用到 ContentProvider,另外我们可以选择要暴露的数据,就避免了我们隐私数据的的泄露!
1.权限申请
从 android6.0 开始,凡是涉及用户隐私的权限(读写短信,读写联系人,拍摄,录音等等),都需要运行时申请,弹窗提醒用户是否授权。用户不授权则无法继续操作
首先在 AndroidManifest.xml 中声明读取通信录的权限
<uses-permission android:name="android.permission.READ_CONTACTS" />
运行时动态申请权限,请求用户授权
class TestContentProviderActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//判断读取通讯录的允许
if(ActivityCompat.checkSelfPermission(this , android.Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){
//如果未授权
ActivityCompat.requestPermissions(
this ,
arrayOf(android.Manifest.permission.READ_CONTACTS) ,
100)
}else{
getContacts() //读取联系人
}
}
//读取联系人
private fun getContacts() {
Toast.makeText(this, "getContacts", Toast.LENGTH_SHORT).show()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode==100 && permissions[0]==android.Manifest.permission.READ_CONTACTS){
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){ //表示授予权限
getContacts()
}else{
Toast.makeText(this,"读取通讯录的权限被拒绝,程序将无法继续工作",Toast.LENGTH_LONG).show()
}
}
}
}
2.读取通讯录联系人
表名 | 说明 |
---|---|
content://com.android.contacts/data/phones | 读取联系人的表的名字 |
字段 | 说明 |
---|---|
display_name | 联系人名 |
data1 | 电话号码 |
//读取联系人
@SuppressLint("Range")
private fun getContacts() {
//查询对象
val resolver = contentResolver
//格式化一个uri
val uri = Uri.parse("content://com.android.contacts/data/phones")
//得到一个游标
val cursor = resolver.query(uri, null, null, null, null)?:return
while (cursor.moveToNext()) {
//获取联系人名字和号码
val displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
Log.e("ContentProvider", "姓名:$displayName")
Log.e("ContentProvider", "号码:$phoneNumber")
Log.e("ContentProvider", "======================")
}
cursor.close() //游标关闭!!
}
3.通信录插入联系人
表名 | 说明 |
---|---|
content://com.android.contacts/data/data | 插入联系人的表的名字 |
content://com.android.contacts/data/raw_contacts | 插入联系人的原始表的名字 |
//插入联系人
private fun insertContact() {
/*
* 首先向RawContacts.CONTENT_URI执行一个空值插入,目的是获得系统返回的rawContactId
* 这时后面插入data表的数据,才能使插入的联系人在通讯录里面可见
*/
val resolver = contentResolver
val values = ContentValues()
val rawContactUri = contentResolver!!.insert(ContactsContract.RawContacts.CONTENT_URI, values)!!
val rawContactId = ContentUris.parseId(rawContactUri)
//往data表里写入姓名数据
values.clear() //先把可能存在的数据清空
values.put(ContactsContract.Data.RAW_CONTACT_ID , rawContactId)
values.put(ContactsContract.Data.MIMETYPE , CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) //内容类型
values.put(CommonDataKinds.StructuredName.GIVEN_NAME , "some_name")
resolver.insert(ContactsContract.Data.CONTENT_URI , values)
//往data表里写入电话数据
values.clear() //先把可能存在的数据清空
values.put(ContactsContract.Data.RAW_CONTACT_ID , rawContactId)
values.put(ContactsContract.Data.MIMETYPE , CommonDataKinds.Phone.CONTENT_ITEM_TYPE) //内容类型
values.put(CommonDataKinds.Phone.NUMBER , "1231231234")
values.put(CommonDataKinds.Phone.TYPE , CommonDataKinds.Phone.TYPE_MOBILE) //手机号
resolver.insert(ContactsContract.Data.CONTENT_URI , values)
//往data表里写入Email的数据
values.clear() //先把可能存在的数据清空
values.put(ContactsContract.Data.RAW_CONTACT_ID , rawContactId)
values.put(ContactsContract.Data.MIMETYPE , CommonDataKinds.Email.CONTENT_ITEM_TYPE) //内容类型
values.put(CommonDataKinds.Email.DATA , "some_name@xx.com")
values.put(CommonDataKinds.Email.TYPE , CommonDataKinds.Email.TYPE_HOME) //家庭邮箱
resolver.insert(ContactsContract.Data.CONTENT_URI , values)
}
4.更新联系人信息
- 根据手机号获取联系人在通讯录的 contact_id
@SuppressLint("Range")
fun getContactIdByPhone(phone : Long) : String?{
val uri = Uri.parse("content://com.android.contacts/data/phones/filter/$phone")
val resolver = contentResolver
val cursor = resolver.query(uri, arrayOf(ContactsContract.Data.CONTACT_ID), null, null, null)?:return null
if (cursor.moveToNext()){
val contractId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID))
return contractId
}
return null
}
- 更新联系人的姓名
//更新联系人
private fun updateContact() {
val contractId = getContactIdByPhone(234524)
if(contractId == null){
Toast.makeText(this , "联系人不存在,无法更新" , Toast.LENGTH_LONG).show()
return
}
val values = ContentValues()
values.put(ContactsContract.Data.MIMETYPE , CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
values.put(CommonDataKinds.StructuredName.GIVEN_NAME, "new_name")
contentResolver.update(
ContactsContract.Data.CONTENT_URI,
values,
"${ContactsContract.Data.CONTACT_ID}=?",
arrayOf(contractId)
)
}
5.删除联系人
- 根据姓名删除联系人
val ret = contentResolver.delete(
ContactsContract.RawContacts.CONTENT_URI,
CommonDataKinds.Phone.DISPLAY_NAME + "=?",
arrayOf("some_name")
)
if(ret>0){
Toast.makeText(this,"删除成功", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "删除失败", Toast.LENGTH_SHORT).show()
}
- 根据手机号删除联系人
val contact_id = getContactIdByPhone(1111)
contentResolver.delete(RawContacts.CONTENT_URI,CommonDataKinds.Phone.CONTACT_ID+"=?", arrayOf(contact_id));
6.读取收件箱所有短信
字段 | 说明 |
---|---|
address | 收件人地址,即手机号,如+8613811810000 |
body | 短信内容 |
date | 日期,long 型,如 1346988516 |
type | 类型,1 是接收到的,2 是已发出 |
read | 是否阅读,0 未读,1 已读 |
protocol | 协议,0SMS_RPOTO 短信,1MMS_PROTO 彩信 |
person | 发件人,如果发件人在通讯录中则为具体姓名,陌生人为 null |
- 需要动态获取权限
<uses-permission android:name="android.permission.READ_SMS"/>
private fun getMessage() {
val uri = Uri.parse("content://sms/")
val resolver = contentResolver
//查询短信的 发件人地址 日期 短信类型 短信具体内容
val cursor = resolver.query(uri, arrayOf("address", "address", "type", "body"), null, null, null)?:return
while (cursor.moveToNext()){
val address = cursor.getString(0)
val data = cursor.getString(1)
val type = cursor.getString(2)
val body = cursor.getString(3)
Log.e("ContentProvider","收件人 $address")
Log.e("ContentProvider","类型时间 [$type]:$data")
Log.e("ContentProvider","短信内容 $body")
Log.e("ContentProvider","-----------------------------")
}
cursor.close() //关闭游标!!
}
短信相关的其它操作 uri content://sms/ 所有短信 content://sms/inbox 收件箱 content://sms/sent 已发送 content://sms/draft 草稿 content://sms/outbox 发件箱 content://sms/failed 发送失败 content://sms/queued 待发送列表