Skip to content

Latest commit

 

History

History
712 lines (551 loc) · 41.5 KB

File metadata and controls

712 lines (551 loc) · 41.5 KB

八、服务、工作管理器和通知

概观

本章将向您介绍在应用的后台管理长期运行的任务的概念。到本章结束时,您将能够触发后台任务,在后台任务完成时为用户创建通知,并从通知启动应用。本章将使您对如何管理后台任务有一个坚实的了解,并让用户了解这些任务的进度。

简介

在前一章中,我们学习了如何向用户请求权限并使用谷歌地图的应用编程接口。有了这些知识,我们获得了用户的位置,并允许他们在本地地图上部署代理。在本章中,我们将学习如何跟踪长时间运行的流程,并向用户报告其进度。

我们将构建一个示例应用,假设秘密猫特工 ( 监控系统)在创纪录的 15 秒内部署完毕。这样,我们将避免在后台任务完成之前等待很长时间。当猫成功部署时,我们会通知用户,让他们启动应用,并向他们显示部署成功的消息。

正在进行的后台任务在移动世界中非常常见。即使应用不活动,后台任务也会运行。长期运行的后台任务包括下载文件、资源清理作业、播放音乐和跟踪用户位置。从历史上看,谷歌为安卓开发者提供了多种执行此类任务的方式:服务、JobScheduler,以及 Firebase 的JobDispatcherAlarmManager。随着安卓世界的碎片化,处理起来相当混乱。幸运的是,自 2019 年 3 月以来,我们有了一个更好(更稳定)的选择。随着WorkManager的推出,谷歌为我们抽象出了基于 API 版本选择后台执行机制的逻辑。我们仍然使用前台服务,这是一种特殊的服务,用于用户在运行时应该知道的某些任务,例如播放音乐或在运行的应用中跟踪用户的位置。

在我们继续之前,先退一步。我们已经提到了服务,我们将专注于前台服务,但是我们还没有完全解释什么是服务。服务是设计为在后台运行的应用组件,即使应用没有运行。除了与通知相关的前台服务之外,其他服务都没有用户界面。需要注意的是,服务运行在其宿主进程的主线程上。这意味着他们的操作可以屏蔽应用。我们应该从服务内部启动一个单独的线程来避免这种情况。

让我们开始看看安卓中管理后台任务的多种方法的实现。

使用工作管理器启动后台任务

我们在这里要解决的第一个问题是,我们应该选择WorkManager还是前台服务?要回答这个问题,一个好的经验法则是问;您是否需要用户实时跟踪该操作?如果答案是肯定的(例如,如果您有一个任务,如响应用户的位置或在后台播放音乐),那么您应该使用前台服务,并附带通知,为用户提供状态的实时指示。当后台任务可以延迟或者不需要用户交互(例如下载大文件)时,使用WorkManager

注意

WorkManager的 2.3.0-alpha02 版本开始,可以通过WorkManager调用setForegroundAsync(ForegroundInfo)启动前台服务。我们对前台服务的控制非常有限。它确实允许您将(预定义的)通知附加到工作中,这就是它值得一提的原因。

在我们的示例中,在我们的应用中,我们将跟踪 ScA 的部署准备情况。在特工出发之前,他们需要伸展身体,梳理毛发,查看垃圾箱,穿好衣服。这些任务中的每一项都需要一些时间。因为你不能催猫,代理会在自己的时间内完成每一步。我们所能做的就是等待(并让用户知道任务何时完成)。WorkManager非常适合这样的场景。

要使用WorkManager,我们需要熟悉它的四个主要类:

  • 首先是WorkManager本身。WorkManager接收工作,并根据提供的参数和约束(如互联网连接和设备收费)将其排队。
  • 第二个是Worker。现在,Worker是需要做的工作的包装器。它有一个功能,doWork(),我们覆盖它来实现后台工作代码。doWork()将在后台线程中执行。
  • 第三类是WorkRequest。这个类将一个Worker类绑定到参数和约束。WorkRequest有两种类型:OneTimeWorkRequest,运行一次工作,PeriodicWorkRequest,可用于安排工作以固定间隔运行。
  • 第四类是ListenableWorker.Result。您可能猜到了,但这是保存已执行工作结果的类。结果可以是SuccessFailureRetry中的一个。

除了这四个类之外,我们还有Data类,它保存着传递给工作者和从工作者传递过来的数据。

让我们回到我们的例子。我们想定义四个需要按顺序发生的任务:猫需要伸展,然后它需要梳理皮毛,然后去猫砂箱,最后,它需要穿好衣服。

在我们开始使用WorkManager之前,我们必须首先在我们的 app build.gradle文件中包含它的依赖关系:

implementation "androidx.work:work-runtime:2.4.0"

随着WorkManager被纳入我们的项目,我们将继续创造我们的工人。第一个工人看起来像这样:

class CatStretchingWorker(
    context: Context,
    workerParameters: WorkerParameters
) : Worker(context, workerParameters) {
    override fun doWork(): Result {
        val catAgentId = inputData.getString(INPUT_DATA_CAT_AGENT_ID)
        Thread.sleep(3000L)
        val outputData = Data.Builder()
            .putString(OUTPUT_DATA_CAT_AGENT_ID, catAgentId)
            .build()
        return Result.success(outputData)
    }
    companion object {
        const val INPUT_DATA_CAT_AGENT_ID = "id"
        const val OUTPUT_DATA_CAT_AGENT_ID = "id"
    }
}

我们首先扩展Worker并覆盖其doWork()功能。然后,我们从输入数据中读取 SCA 标识。然后,因为我们没有真正的传感器来跟踪猫伸展的进度,我们通过引入 3 秒(3000 毫秒)Thread.sleep(Long)呼叫来假装我们的等待。最后,我们用在输入中收到的 ID 构造一个输出数据类,并返回成功的结果。

一旦我们为所有任务创建了工人(CatStretchingWorkerCatFurGroomingWorkerCatLitterBoxSittingWorkerCatSuitUpWorker,类似于我们创建第一个任务的方式,我们可以调用WorkManager来链接他们。让我们也假设我们不能告诉代理的进度,除非我们连接到互联网。我们的电话应该是这样的:

val catStretchingInputData = Data.Builder()
  .putString(CatStretchingWorker.INPUT_DATA_CAT_AGENT_ID, 
    "catAgentId").build()
val catStretchingRequest = OneTimeWorkRequest
  .Builder(CatStretchingWorker::class.java)
val catStretchingRequest =   OneTimeWorkRequest.Builder(CatStretchingWorker::class.java)
    .setConstraints(networkConstraints)
    .setInputData(catStretchingInputData)
    .build()
...
WorkManager.getInstance(this).beginWith(catStretchingRequest)
    .then(catFurGroomingRequest)
    .then(catLitterBoxSittingRequest)
    .then(catSuitUpRequest)
    .enqueue()

在前面的代码中,我们首先构建一个Constraints实例,声明我们需要连接到互联网才能执行工作。然后我们定义我们的输入数据,将其设置为 SCA ID。接下来,我们通过构造OneTimeWorkRequest将约束和输入数据绑定到我们的Worker类。其他WorkRequest实例的构造被省略了,但是它们与这里显示的非常相似。我们现在可以将所有请求链接起来,并在WorkManager类中排队。您可以通过将单个WorkRequest实例直接传递给WorkManager enqueue()函数来将其入队,也可以将多个WorkRequest实例作为列表传递给WorkManager enqueue()函数来并行运行。

当满足约束时,我们的任务将由WorkManager执行。

每个Request实例都有唯一的标识符。WorkManager为每个请求公开一个LiveData属性,允许我们通过传递其唯一标识符来跟踪其工作进度,如以下代码所示:

workManager.getWorkInfoByIdLiveData(catStretchingRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            doSomething()
        }
    })

工作状态可以是BLOCKED(有请求链,不是链中的下一个)ENQUEUED(有请求链,这个工作是下一个)RUNNING(在doWork()的工作正在执行中)SUCCEEDED。工作也可以取消,导致CANCELLED状态,也可以失败,导致FAILED状态。

最后还有Result.retry。返回这个结果告诉WorkManager类再次将工作排队。管理何时再次运行工作的策略由在WorkRequest Builder上设置的backoff标准定义。默认的backoff策略是指数的,但是我们可以设置为线性的。我们也可以定义初始backoff时间。

让我们在下面的练习中实践到目前为止所获得的知识。

在本节中,我们将跟踪我们的 SCA,从我们发出命令将它部署到现场到它到达目的地。

练习 8.01:使用工作管理器类执行后台工作

在第一个练习中,我们将跟踪 SCA,因为它准备通过将连锁WorkRequest 类入队出去:

  1. 首先创建一个新的Empty Activity项目(File -> New -> New Project -> Empty Activity)。点击Next

  2. 说出你的申请Cat Agent Tracker

  3. 确保您的套餐名称为com.example.catagenttracker

  4. 将保存位置设置为要保存项目的位置。

  5. 将其他所有内容保留为默认值,然后单击Finish

  6. 确保您在Project窗格中的安卓视图上。

  7. Open your app's build.gradle file. In the dependencies block, add the WorkManager dependency:

    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
        ...
        implementation "androidx.work:work-runtime:2.4.0"
        ...
    }

    这将允许您在代码中使用WorkManager及其依赖项。

  8. 在你的应用包下新建一个包(右键点击com.example.catagenttracker,然后New | Package)。命名新套餐com.example.catagenttracker.worker

  9. com.example.catagenttracker.worker下新建一个名为CatStretchingWorker的类(右键点击worker,然后New | New Kotlin File/Class)。在Kind下,选择Class

  10. To define a Worker instance that will sleep for 3 seconds, update the new class like so:

```kt
package com.example.catagenttracker.worker
import android.content.Context
import androidx.work.Data
import androidx.work.Worker
import androidx.work.WorkerParameters
class CatStretchingWorker(
    context: Context,
    workerParameters: WorkerParameters
) : Worker(context, workerParameters) {
    override fun doWork(): Result {
        val catAgentId = inputData.getString(INPUT_DATA_CAT_AGENT_ID)
        Thread.sleep(3000L)
        val outputData = Data.Builder()
            .putString(OUTPUT_DATA_CAT_AGENT_ID, catAgentId)
            .build()
        return Result.success(outputData)
    }
    companion object {
        const val INPUT_DATA_CAT_AGENT_ID = "inId"
        const val OUTPUT_DATA_CAT_AGENT_ID = "outId"
    }
}
```

这将为`Worker`实现添加所需的依赖项,然后扩展`Worker`类。为了实现实际工作,您将覆盖`doWork(): Result`,使其从输入中读取卡特彼勒代理商标识,休眠`3`秒(`3000`毫秒),用卡特彼勒代理商标识构建输出数据实例,并将其传递到`Result.success`值中。
  1. 重复第 9 步和第 10 步,创建三个相同的名为CatFurGroomingWorkerCatLitterBoxSittingWorkerCatSuitUpWorker的工人。
  2. Open MainActivity. Right before the end of the class, add the following:
```kt
private fun getCatAgentIdInputData(catAgentIdKey: String,   catAgentIdValue: String) =
    Data.Builder().putString(catAgentIdKey, catAgentIdValue)
        .build()
```

该辅助函数为您构建了一个输入`Data`实例,带有卡特彼勒代理商标识。
  1. Add the following to the onCreate(Bundle?) function:
```kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val networkConstraints =
        Constraints.Builder()          .setRequiredNetworkType(NetworkType.CONNECTED).build()
    val catAgentId = "CatAgent1"
    val catStretchingRequest =       OneTimeWorkRequest.Builder
 (CatLitterBoxSittingWorker::class.java)
        .setConstraints(networkConstraints)
        .setInputData(
            getCatAgentIdInputData(CatStretchingWorker               .INPUT_DATA_CAT_AGENT_ID, catAgentId)
        ).build()
    val catFurGroomingRequest =       OneTimeWorkRequest.Builder(CatFurGroomingWorker::class.java)
        .setConstraints(networkConstraints)
        .setInputData(
            getCatAgentIdInputData(CatFurGroomingWorker               .INPUT_DATA_CAT_AGENT_ID, catAgentId)
        ).build()
    val catLitterBoxSittingRequest =
        OneTimeWorkRequest.Builder           (CatLitterBoxSittingWorker::class.java)
            .setConstraints(networkConstraints)
            .setInputData(
                getCatAgentIdInputData(
                    CatLitterBoxSittingWorker                       .INPUT_DATA_CAT_AGENT_ID,
                    catAgentId
                )
            ).build()
    val catSuitUpRequest =       OneTimeWorkRequest.Builder(CatSuitUpWorker::class.java)
        .setConstraints(networkConstraints)
        .setInputData(
            getCatAgentIdInputData(CatSuitUpWorker               .INPUT_DATA_CAT_AGENT_ID, catAgentId)
        ).build()
}
```

添加的第一行定义了网络约束。它告诉`WorkManager`类在执行工作之前等待互联网连接。然后,定义您的卡特彼勒代理商标识。最后,您定义四个请求,以输入数据的形式传入您的`Worker`类、网络约束和卡特彼勒代理商标识。
  1. 在课程顶部,定义您的WorkManager :
```kt
private val workManager = WorkManager.getInstance(this)
```
  1. Add a chained enqueue request right below the code you just added, still within the onCreate function:
```kt
val catSuitUpRequest =   OneTimeWorkRequest.Builder(CatSuitUpWorker::class.java)
    .setConstraints(networkConstraints)
    .setInputData(
        getCatAgentIdInputData(CatSuitUpWorker           .INPUT_DATA_CAT_AGENT_ID, catAgentId)
    ).build()
workManager.beginWith(catStretchingRequest)
    .then(catFurGroomingRequest)
    .then(catLitterBoxSittingRequest)
    .then(catSuitUpRequest)
    .enqueue()
```

当您的`WorkRequests`满足它们的约束条件并且`WorkManager`类准备好执行它们时,您的`WorkRequests`现在被排队按顺序执行。
  1. 定义一个函数,用提供的消息显示祝酒词。应该是这样的:
```kt
private fun showResult(message: String) {
    Toast.makeText(this, message, LENGTH_SHORT).show()
}
```
  1. To track the progress of the enqueued WorkRequest instances, add the following after the enqueue call:
```kt
workManager.beginWith(catStretchingRequest)
    .then(catFurGroomingRequest)
    .then(catLitterBoxSittingRequest)
    .then(catSuitUpRequest)
    .enqueue()
workManager.getWorkInfoByIdLiveData(catStretchingRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            showResult("Agent done stretching")
        }
    })
workManager.getWorkInfoByIdLiveData(catFurGroomingRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            showResult("Agent done grooming its fur")
        }
    })
workManager.getWorkInfoByIdLiveData(catLitterBoxSittingRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            showResult("Agent done sitting in litter box")
        }
    })
workManager.getWorkInfoByIdLiveData(catSuitUpRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            showResult("Agent done suiting up. Ready to go!")
        }
    })
```

前面的代码观察到了由`WorkManager`类为每个`WorkRequest`提供的`WorkInfo`可观测值。当每个请求完成时,会显示一个带有相关消息的祝酒词。
  1. 运行您的应用:

Figure 8.1: Toasts showing in order

图 8.1:按顺序显示的祝酒词

您现在应该会看到一个简单的Hello World!屏幕。但是,如果您等待几秒钟,您将开始看到祝酒词,通知您您的 SCA 准备部署到现场的进度。您会注意到祝酒词遵循您将请求排队并按顺序执行它们的延迟的顺序。

用户可注意到的后台操作–使用前台服务

随着我们的 SCA 全部穿好衣服,他们现在已经准备好到达指定的目的地。为了跟踪 SCA,我们将使用前台服务定期轮询 SCA 的位置,并用新的位置更新附加到该服务的粘性通知(用户不能拒绝的通知)。为了简单起见,我们将伪造位置。根据您在第 7 章安卓权限和谷歌地图中所学的内容,您可以稍后用一个使用地图的真实实现来替换这个实现。

前台服务是执行后台操作的另一种方式。这个名字可能有点反直觉。它旨在将这些服务与基本的安卓(后台)服务区分开来。前者与通知相关联,而后者在后台运行,没有内置面向用户的表示。前台服务和后台服务的另一个重要区别是,当系统内存不足时,后台服务是终止的候选,而前台服务不是。

从 Android 9 (Pie,或 API 级别 28)开始,我们必须请求FOREGROUND_SERVICE许可才能使用前台服务。因为是正常权限,所以会自动授予我们的 app。

在我们启动前台服务之前,我们必须先创建一个。前台服务是安卓抽象Service类的子类。如果我们不打算绑定到服务,并且在我们的示例中,我们确实不打算绑定,我们可以简单地覆盖onBind(Intent)以便它返回null。另外,绑定是感兴趣的客户端与服务进行通信的方式之一。在本书中,我们不会关注这种方法,因为还有其他更简单的方法,正如您将在下面发现的。

前台服务必须绑定到通知。在 Android 8 (Oreo,或 API 级别 26)及以上版本上,如果前台服务在应用无响应 ( ANR )时间窗口内(约 5 秒)未绑定到某个服务,则该服务将被停止,该应用将被声明为无响应。由于这一要求,我们最好尽快将服务与通知联系起来。最好的地方是服务的onCreate()功能。快速实现如下所示:

private fun onCreate() {
    val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val newChannelId = "ChannelId"
        val channelName = "My Background Service"
        val channel =
            NotificationChannel(newChannelId, channelName,               NotificationManager.IMPORTANCE_DEFAULT)
        val service = getSystemService(Context.NOTIFICATION_SERVICE) as           NotificationManager
        service.createNotificationChannel(channel)
        newChannelId
    } else {
        ""
    }
    val pendingIntent = Intent(this, MainActivity::class.java).let {       notificationIntent ->
        PendingIntent.getActivity(this, 0, notificationIntent, 0)
    }
    val notification = NotificationCompat.Builder(this, channelId)
        .setContentTitle("Content title")
        .setContentText("Content text")
        .setSmallIcon(R.drawable.notification_icon)
        .setContentIntent(pendingIntent)
        .setTicker("Ticker message")
        .build()
    startForeground(NOTIFICATION_ID, notificationBuilder.build())
}

让我们把它分解一下。

我们从定义频道标识开始。这仅在 Android Oreo 或以上版本中是必需的,在 Android 的早期版本中被忽略。在安卓奥利奥中,谷歌引入了渠道的概念。通道用于对通知进行分组,并允许用户过滤掉不需要的通知:

    val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val newChannelId = "ChannelId"
        val channelName = "My Background Service"
        val channel =
            NotificationChannel(newChannelId, channelName,               NotificationManager.IMPORTANCE_DEFAULT)
        val service = getSystemService(Context.NOTIFICATION_SERVICE) as           NotificationManager
        service.createNotificationChannel(channel)
        newChannelId
    } else {
        ""
    }

接下来,我们定义pendingIntent。如果用户点击通知,这将是启动的意图。在本例中,将启动主要活动:

    val pendingIntent = Intent(this, MainActivity::class.java).let {       notificationIntent ->
        PendingIntent.getActivity(this, 0, notificationIntent, 0)
    }

有了频道标识和pendingIntent,我们就可以构建我们的通知了。我们使用NotificationCompat,它去掉了一些关于支持旧的应用编程接口级别的样板文件。我们传入服务作为上下文和通道标识。我们定义标题、文本、小图标、意图和滚动条消息,并构建通知:

    val notification = NotificationCompat.Builder(this, channelId)
        .setContentTitle("Content title")
        .setContentText("Content text")
        .setSmallIcon(R.drawable.notification_icon)
        .setContentIntent(pendingIntent)
        .setTicker("Ticker message")
        .build()

要在前台启动一个服务,将通知附加到它上面,我们称之为startForeground(Int, Notification)函数,传递一个通知标识(任何唯一的 int 值来标识这个服务,它不能是 0)和一个通知,它的优先级必须设置为PRIORITY_LOW或更高。在我们的情况下,我们没有指定优先级,这将它设置为PRIORITY_DEFAULT:

    startForeground(NOTIFICATION_ID, notificationBuilder.build())

如果启动,我们的服务现在将显示一个粘性通知。点击通知将启动我们的主要活动。然而,我们的服务不会做任何有用的事情。为了给它增加一些功能,我们需要覆盖onStartCommand(Intent?, Int, Int)。当服务通过一个意图启动时,这个函数被调用,这也给了我们机会去读取通过那个意图传递的任何额外数据。它还为我们提供了标志(可以设置为START_FLAG_REDELIVERYSTART_FLAG_RETRY)和唯一的请求标识。我们将在本章稍后开始阅读额外的数据。在一个简单的实现中,您不需要担心标志或请求标识。

需要注意的是onStartCommand(Intent?, Int, Int)是在 UI 线程上被调用的,所以不要在这里执行任何长时间运行的操作,否则你的 app 会冻结,给用户带来不好的体验。相反,我们可以使用一个新的HandlerThread(一个带有循环的线程,一个用于为线程运行消息循环的类)创建一个新的处理程序,并将我们的工作发布给它。这意味着我们将有一个无限循环运行,等待我们通过Handler发布到它。当我们收到启动命令时,我们可以向它发布我们想要完成的工作。然后,该工作将在该线程上执行。

当我们长期的工作完成后,有一些事情我们可能希望发生。首先,我们可能想通知任何感兴趣的人(例如,我们的主要活动,如果它正在运行)我们已经完成了。然后,我们可能想停止在前台运行。最后,如果我们不希望再次需要该服务,我们可以停止它。

应用有几种与服务通信的方式——绑定、使用广播接收器、使用总线架构或使用结果接收器,等等。例如,我们将使用谷歌的LiveData

在我们继续之前,值得一提的是广播接收器。广播接收器允许我们的应用使用类似于发布-订阅设计模式的模式发送和接收消息。

系统广播设备启动或充电开始等事件。我们的服务也可以广播状态更新。例如,他们可以在完成时广播一个长的计算结果。

如果我们的应用注册接收某个消息,系统会在该消息广播时通知它。

这曾经是与服务通信的一种常见方式,但是LocalBroadcastManager类现在被否决了,因为它是一种鼓励反模式的应用范围的事件总线。

话虽如此,广播接收器对于全系统事件仍然有用。我们首先定义一个覆盖BroadcastReceiver抽象类的类:

class ToastBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        StringBuilder().apply {
            append("Action: ${intent.action}\n")
            append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
            toString().let { eventText ->
                Toast.makeText(context, eventText,                   Toast.LENGTH_LONG).show()
            }
        }
    }
}

ToastBroadcastReceiver收到一个事件时,会显示一个祝酒词,表示该事件的动作和 URI。

我们可以通过Manifest.xml文件注册我们的接收器:

<receiver android:name=".ToastBroadcastReceiver" android:exported="true">
    <intent-filter>
        <action android:name=          "android.intent.action.ACTION_POWER_CONNECTED" />
    </intent-filter>
</receiver>

指定android:exported="true"告诉系统该接收器可以从应用外部接收消息。动作定义了我们感兴趣的信息。我们可以指定多个动作。在这个例子中,我们监听设备何时开始充电。请记住,将此值设置为“真”将允许其他应用(包括恶意应用)激活此接收器。

我们还可以用代码注册消息:

val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
    addAction(Intent.ACTION_POWER_CONNECTED)
}
registerReceiver(ToastBroadcastReceiver(), filter)

将此代码添加到活动或我们的自定义应用类中,也将注册我们的接收器的新实例。只要上下文(活动或应用)有效,这个接收器就会一直存在。因此,相应地,如果活动或应用被销毁,我们的接收器将被释放出来进行垃圾收集。

现在回到我们的实现。要在我们的应用中使用LiveData,我们必须在app/build.gradle文件中添加一个依赖项:

Dependencies {
    ...
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    ...
}

然后我们可以在服务的伴随对象中定义一个LiveData实例,如下所示:

companion object {
    private val mutableWorkCompletion = MutableLiveData<String>()
    val workCompletion: LiveData<String> = mutableWorkCompletion
}

请注意,我们将MutableLiveData实例隐藏在一个LiveData接口后面。这是为了让消费者只能读取数据。我们现在可以使用mutableWorkCompletion实例通过给它赋值来报告完成。但是,我们必须记住,值只能分配给主线程上的LiveData实例。这意味着一旦我们的工作完成,我们必须切换回主线程。我们可以很容易地实现这一点——我们所需要的是一个带有主Looper(通过调用Looper.getMainLooper()获得)的新处理器,我们可以向它发布我们的更新。

现在我们的服务已经准备好做一些工作,我们终于可以启动它了。在此之前,我们必须确保在<application></application>块中将服务添加到我们的AndroidManifest.xml文件中,如以下代码所示:

<application ...>
    <service android:name=".ForegroundService" />
</application>

为了启动我们刚刚添加到清单中的服务,我们创建了Intent,传递任何所需的额外数据,如以下代码所示:

val serviceIntent = Intent(this, ForegroundService::class.java).apply {
    putExtra("ExtraData", "Extra value")
}

然后我们调用ContextCompat.startForegroundService(Context, Intent)启动Intent并启动服务。

练习 8.02:使用前台服务跟踪您的 SCA 工作

在第一个练习中,当 SCA 准备使用WorkManager类离开时,您跟踪了它。在本练习中,您将在 SCA 部署到现场并向指定目标移动时对其进行跟踪,方法是显示一个粘性通知,倒计时到达目的地的时间。该通知将由前台服务驱动,前台服务将呈现并持续更新该通知。如果通知尚未运行,则随时单击通知将启动您的主要活动,并始终将其带到前台:

  1. 首先,通过更新应用的build.gradle文件

    implementation "androidx.work:work-runtime:2.4.0"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

    ,将LiveData依赖项添加到项目中

  2. Then, create a new class called RouteTrackingService, extending the abstract Service class:

    class RouteTrackingService : Service() {
        override fun onBind(intent: Intent): IBinder? = null
    }

    在这个练习中你不会依赖绑定,所以在onBind(Intent)实现中简单的返回null是安全的。

  3. In the newly created service, define some constants that you will later need, as well as the LiveData instance used to observe progress:

    companion object {
        const val NOTIFICATION_ID = 0xCA7
        const val EXTRA_SECRET_CAT_AGENT_ID = "scaId"
        private val mutableTrackingCompletion = MutableLiveData<String>()
        val trackingCompletion: LiveData<String> = mutableTrackingCompletion
    }

    NOTIFICATION_ID必须是该服务拥有的通知的唯一标识符,不得是0。现在,EXTRA_SECRET_CAT_AGENT_ID是你用来传递数据给服务的常量。mutableTrackingCompletion是私有的,用于允许您通过LiveData在内部发布完成更新,而不会暴露服务之外的可变性。trackingCompletion然后用于以不变的方式暴露LiveData实例进行观察。

  4. Add a function to your RouteTrackingService class to provide PendingIntent to your sticky notification:

    private fun getPendingIntent() =
        PendingIntent.getActivity(this, 0, Intent(this,       MainActivity::class.java), 0)

    这将在用户点击Notification时启动MainActivity。你调用PendingIntent.getActivity(),传递一个上下文,没有请求代码(0),Intent会启动MainActivity,也没有标志(0)。你回来PendingIntent,它将启动那个活动。

  5. Add another function to create NotificationChannel for devices running Android Oreo or newer:

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createNotificationChannel(): String {
        val channelId = "routeTracking"
        val channelName = "Route Tracking"
        val channel =
            NotificationChannel(channelId, channelName,           NotificationManager.IMPORTANCE_DEFAULT)
        val service = getSystemService(Context.NOTIFICATION_SERVICE) as       NotificationManager
        service.createNotificationChannel(channel)
        return channelId
    }

    您可以从定义频道标识开始。这需要对一个包是唯一的。接下来,定义一个用户可见的频道名称。这可以(也应该)本地化。为了简单起见,我们跳过了这一部分。然后创建一个重要性设置为IMPORTANCE_DEFAULTNotificationChannel实例。重要性决定了发布到此渠道的通知的破坏性。最后,使用Notification Service创建一个通道,数据在NotificationChannel实例中提供。该函数返回通道标识,以便用于构建Notification

  6. Create a function to provide you with Notification.Builder:

    private fun getNotificationBuilder(pendingIntent: PendingIntent, channelId: String) =
        NotificationCompat.Builder(this, channelId)
            .setContentTitle("Agent approaching destination")
            .setContentText("Agent dispatched")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentIntent(pendingIntent)
            .setTicker("Agent dispatched, tracking movement")

    该函数采用从您之前创建的函数生成的pendingIntentchannelId实例,并构造一个NotificationCompat.Builder类。生成器允许您定义标题(第一行)、文本(第二行)、要使用的小图标(大小因设备而异)、用户单击Notification时要触发的意图以及滚动条(用于辅助功能;在安卓棒棒糖之前,这在通知出现之前就已经显示出来了)。您也可以设置其他属性。探索NotificationCompat.Builder课。在实际项目中,请记住使用 strings.xml 中的字符串资源,而不是硬编码的字符串。

  7. Implement the following code to introduce a function to start the foreground service:

    private fun startForegroundService(): NotificationCompat.Builder {
        val pendingIntent = getPendingIntent()
        val channelId =       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
        } else {
            ""
        }
        val notificationBuilder = getNotificationBuilder(pendingIntent,       channelId)
        startForeground(NOTIFICATION_ID, notificationBuilder.build())
        return notificationBuilder
    }

    首先使用前面介绍的函数得到PendingIntent。然后,根据设备的应用编程接口级别,您创建一个通知通道并获取其标识或设置一个空标识。您将pendingIntentchannelId传递给构建NotificationCompat.Builder的函数,并作为前台服务启动该服务,为其提供NOTIFICATION_ID和使用构建器构建的通知。该功能返回NotificationCompat.Builder,稍后用于更新通知。

  8. 在您的服务中定义两个字段——一个保存可重用的NotificationCompat.Builder类,另一个保存对Handler的引用,稍后您将使用它在后台发布工作:

    private lateinit var notificationBuilder: NotificationCompat.Builder
    private lateinit var serviceHandler: Handler
  9. Next, override onCreate() to start the service as a foreground service, keep a reference to the Notification.Builder, and create serviceHandler:

    override fun onCreate() {
        super.onCreate()
        notificationBuilder = startForegroundService()
        val handlerThread = HandlerThread("RouteTracking").apply {        start() 
        }
        serviceHandler = Handler(handlerThread.looper)
    }

    请注意,要创建Handler实例,必须首先定义并启动HandlerThread

  10. Define a call that tracks your deployed SCA as it approaches its designated destination:

```kt
private fun trackToDestination(notificationBuilder:   NotificationCompat.Builder) {
    for (i in 10 downTo 0) {
        Thread.sleep(1000L)
        notificationBuilder           .setContentText("$i seconds to destination")
        startForeground(NOTIFICATION_ID,           notificationBuilder.build())
    }
}
```

这将从`10`倒计时到`1`,在更新之间休眠 1 秒,然后用剩余时间更新通知。
  1. Add a function to notify observers of completion on the main thread:
```kt
private fun notifyCompletion(agentId: String) {
    Handler(Looper.getMainLooper()).post {
        mutableTrackingCompletion.value = agentId
    }
}
```

通过使用主`Looper`在处理程序上发帖,您可以确保更新发生在主(用户界面)应用线程上。将该值设置为代理标识时,您将通知所有观察者该代理标识已到达其目的地。
  1. Override onStartCommand(Intent?, Int, Int) like so:
```kt
override fun onStartCommand(intent: Intent?, flags: Int,   startId: Int): Int {
    val returnValue = super.onStartCommand(intent, flags, startId)
    val agentId =       intent?.getStringExtra(EXTRA_SECRET_CAT_AGENT_ID)
        ?: throw IllegalStateException("Agent ID must be provided")
    serviceHandler.post {
        trackToDestination(notificationBuilder)
        notifyCompletion(agentId)
        stopForeground(true)
        stopSelf()
    }
    return returnValue
}
```

您首先将调用委托给`super`,它在内部调用`onStart()`,并返回您可以返回的向后兼容状态。您存储这个返回值。接下来,您从通过意图传递的附加信息中获取 SCA 标识。如果没有代理 ID,这个服务将无法工作,所以如果没有提供代理 ID,您将抛出一个异常。接下来,您切换到`onCreate`中定义的后台线程,以阻塞方式跟踪代理到其目的地。跟踪完成后,您通知观察者任务已完成,停止前台服务(通过传递`true`移除通知),并停止服务本身,因为您预计不会很快再次需要它。然后从`super`返回先前存储的返回值。
  1. Update your AndroidManifest.xml to request the FOREGROUND_SERVICE permission and introduce the service:
```kt
<manifest ...>
    <uses-permission android:name=      "android.permission.FOREGROUND_SERVICE"/>
    <application ...>
        <service
            android:name=".RouteTrackingService"
            android:enabled="true"
            android:exported="true" />
        <activity ...>
```

首先,我们声明我们的应用需要`FOREGROUND_SERVICE`许可。除非我们这样做,否则系统将阻止我们的应用使用前台服务。接下来,我们声明服务。设置`android:enabled="true"`告诉系统可以实例化服务。默认为`"true"`,所以这是可选的。用`android:exported="true"`定义服务告诉系统其他应用可以启动服务。在我们的例子中,我们不需要这个额外的功能,但是我们添加它只是为了让您知道这个功能。
  1. Back to your MainActivity. Introduce a function to launch RouteTrackingService:
```kt
private fun launchTrackingService() {
    RouteTrackingService.trackingCompletion.observe(this, Observer {       agentId ->
        showResult("Agent $agentId arrived!")
    })
    val serviceIntent = Intent(this,       RouteTrackingService::class.java).apply {
        putExtra(EXTRA_SECRET_CAT_AGENT_ID, "007")
    }
    ContextCompat.startForegroundService(this, serviceIntent)
}
```

该功能首先观察`LiveData`完成更新,显示完成结果。然后,它定义`Intent`来启动服务,将 SCA 标识设置为该`Intent`的额外参数。然后它使用`ContextCompat`作为前台服务启动服务,这为您隐藏了兼容性相关的逻辑。
  1. 最后,更新onCreate(),一旦 SCA 适合并准备就绪,就开始跟踪它:
```kt
workManager.getWorkInfoByIdLiveData(catSuitUpRequest.id)
    .observe(this, Observer { info ->
        if (info.state.isFinished) {
            showResult("Agent done suiting up. Ready to go!")
            launchTrackingService()
        }
    })
```
  1. 启动应用:

Figure 8.2: Notification counting down

图 8.2:倒计时通知

在通知您 SCA 的准备步骤之后,您应该会在状态栏中看到通知。然后,该通知应该从 10 倒数到 0,消失,并由祝酒词代替,通知您代理已到达目的地。看到最后的祝酒词告诉您,您已经成功地将 SCA ID 传递给了服务,并在完成后台任务后将其取回。

利用从本章中获得的所有知识,让我们完成以下活动。

活动 8.01:提醒喝水

人类平均每天流失约 2500 毫升水(见https://en.wikipedia.org/wiki/Fluid_balance#Output)。为了保持健康,我们需要消耗尽可能多的水分。然而,由于现代生活的繁忙性质,我们很多人忘记了定期保持水分。假设你想开发一个应用来跟踪你的水分流失(统计数据),并不断更新你的液体平衡。从平衡状态开始,应用会逐渐降低用户的跟踪水位。用户可以告诉应用他们什么时候喝了一杯水,它会相应地更新水位。水位的持续更新将利用您运行后台任务的知识,并且您还将利用您与服务通信的知识来更新余额以响应用户交互。

以下步骤将帮助您完成活动:

  1. 创建一个空的活动项目并命名你的应用My Water Tracker
  2. 向您的AndriodManifest.xml文件添加前台服务权限。
  3. 创建新服务。
  4. 在服务中定义一个变量来跟踪水位。
  5. 为通知标识和额外的意图数据键定义常量。
  6. 设置从服务创建通知。
  7. 添加启动前台服务和更新水位的功能。
  8. 将水位设置为每 5 秒降低一次。
  9. 处理服务外液体的添加。
  10. 确保服务在被销毁时清除回调和消息。
  11. Manifest.xml文件中注册服务。
  12. 创建活动后,从MainActivity开始服务。
  13. 向主活动布局添加按钮。
  14. When the user clicks the button, notify the service that it needs to increment the water level.
注意

这个活动的解决方案可以在:[http://packt.live/3sKj1cp](08.html)找到

总结

在本章中,我们学习了如何使用WorkManager和前台服务执行长时间运行的后台任务。我们讨论了如何向用户传达进度,以及如何在任务完成后让用户返回应用。本章涵盖的所有主题都非常广泛,您可以探索与服务通信、构建通知以及进一步使用WorkManager类。希望对于大多数常见的场景,您现在已经拥有了所需的工具。常见的使用案例包括后台下载、后台清理缓存素材、在应用不在前台运行时播放音乐,以及结合我们从第 7 章安卓权限和谷歌地图中获得的知识,随着时间的推移跟踪用户的位置。

在下一章中,我们将通过编写单元测试和集成测试来使我们的应用更加健壮和可维护。当您编写的代码在后台运行时,这一点特别有用,当出现问题时,这一点不会立即显现出来。