如何在系统范围内使用 WorkManger 注册一次定期工作请求(即在启动或安装后)

How to register a periodic work request with WorkManger system-wide once (i.e. after boot or installation)

我的 Android 应用程序中需要一个组件,最好将其描述为看门狗,即每 30 分钟 +/- 5 分钟执行一次并断言仍满足特定条件的功能。看门狗也必须在设备重启后执行,而用户此后没有明确打开应用程序。应用程序的安装也必须如此。必须安排看门狗定期执行,即使应用程序在安装后未明确打开。

我知道使用 WorkManager 是最好的或 "modern" 方式。没有 WorkManager 我必须为不同的 API 级别编写单独的代码,即使用 BroadcastReceiver 用于 API 级别 <27 的设备和 JobScheduler 用于更高 API 级别。 WorkManager 应该抽象掉这些差异。

但是我不明白在哪里调用WorkManager.getInstance().enqueue( myWatchdogRequest );。使用任何主要活动的回调(即 onCreate 和类似的)都不是正确的地方,因为我不能依赖 activity 曾经被创建。

我预计除了以编程方式对作业进行排队外,还应该有一种方法可以在清单中声明这些作业,从而将它们通知给系统(类似于老式的 BroadcastReceiver)。实际上,如果我决定使用那种方法,JobScheduler 也会遇到同样的问题。

我在哪里排队 WorkRequest “全局”?

在第一部分中,我只是将解决方案作为代码片段展示,没有太多解释。在第二部分,我详细阐述了解决方案,解释了为什么它不是一个精确的解决方案,而是最好的解决方案,并指出了 Google 文档中的一些错误,这些错误首先导致了我的问题。

解决方案

每 30 分钟 运行 灵活 10 分钟的实际工人:

public class WatchDogWorker extends Worker {
  private static final String uniqueWorkName = "my.package.name.watch_dog_worker";
  private static final long repeatIntervalMin = 30;
  private static final long flexIntervalMin = 10;

  public WatchDogWorker( @NonNull Context context, @NonNull WorkerParameters params) {
    super( context, params );
  }

  private static PeriodicWorkRequest getOwnWorkRequest() {
    return new PeriodicWorkRequest.Builder(
      WatchDogWorker.class, repeatIntervalMin, TimeUnit.MINUTES, flexIntervalMin, TimeUnit.MINUTES
    ).build();
  }

  public static void enqueueSelf() {
    WorkManager.getInstance().enqueueUniquePeriodicWork( uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, getOwnWorkRequest() );
  }

  public Worker.Result doWork() {
    // Put the actual code of the watchdog that needs to be run every 30mins here
    return Result.SUCCESS;
  }
}

注: a) 由于这个worker需要以相同的方式在两个不同的执行点(见下文)注册调度,我决定WatchDogWorker 应该"know" 如何自己入队。因此它提供了静态方法getOwnWorkRequestenqueueSelf。 b) 私有的静态常量只需要一次,但使用常量可以避免代码中的幻数,并赋予数字语义意义。

要在设备启动后排队 WatchDogWorker 进行调度,需要以下广播接收器:

public class BootCompleteReceiver extends BroadcastReceiver {
  public void onReceive( Context context, Intent intent ) {
    if( intent.getAction() == null || !intent.getAction().equals( "android.intent.action.BOOT_COMPLETED" ) ) return;
    WatchDogWorker.enqueueSelf();
  }
}

从本质上讲,整个魔术是一行并调用 WatchDogWorker.enqueueSelf。广播接收器应该在启动后被调用一次。为此,必须在 AndroidManifest.xml 中声明广播接收器,以便 Android 系统知道接收器并在启动时调用它:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="...">

  ...

  <application>
    ...
    <receiver
      android:name=".BootCompleteReceiver"
      android:enabled="true"
      android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    </receiver>
  </application>
</manifest>

然而,这还不够。如果用户刚安装了应用程序,我们不想等到下次重启时才第一次安排看门狗,但我们希望尽快安排。因此,如果创建了主 activity,WatchDogWorker 也会入队。

public class MainActivity extends AppCompatActivity {
  ...
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    // Schedule WatchDogWorker (after a fresh install we must not rely on the BootCompleteReceiver)
    WatchDogWorker.enqueueSelf();
  }
}

注意: 此解决方案可能会多次调用方法 WatchDogWorker.enqueueSelf。但是,enqueueSelf 在内部调用 enqueueUniquePeriodicWorkExistingPeriodicWorkPolicy.KEEP。因此,后续调用 enqueueSelf 是空操作,不会造成任何伤害。

警告: 所提供的解决方案仅为 95% 的解决方案。如果用户在安装后从未启动应用程序,即从未创建 activity,则 WatchDogWorker 永远不会排队,也永远不会 运行。即使,如果设备最终在未来的某个时间点重新启动(但应用程序从未启动过),也永远不会收到 "boot complete" 意图并且 WatchDogWorker 也不会排队。这种情况没有解决方法。 (见下一章。)

其他背景信息

导致我提出问题的第一个问题是如果设备已重新启动而不依赖于要创建的 activity,如何使工作人员排队。我确实了解广播接收器,尤其是 BOOT_COMPLETED-intent。但根据 official Android documentation nearly all broadcast receivers have been radically disabled beginning with Android 8. This measurement was part of Google's attempt to improve the power management. In the past, broadcast receivers have allegedly been abused by many less-skilled developers to do insane things that should have been better done in some other way. (Trivial example: Misuse the AlarmManager and the corresponding broadcast receiver to wake up your app every 500ms, simply to check if there are updates available on your server.) Google's countermeasure was to simply cut-off those broadcast receivers. More precisely, a quote from the docs:

Beginning with Android 8.0 [...], the system imposes [...] restrictions on manifest-declared receivers. [...] you cannot use the manifest to declare a receiver for most implicit broadcasts (broadcasts that don't target your app specifically). You can still use a context-registered receiver when the user is actively using your app.

有两个方面很重要:限制适用于隐含的意图。不幸的是,BOOT_COMPLETED 意图是隐式意图 according to docs。其次,可以克服此限制,但只能通过编程方式或换句话说通过您的 activity 的某些已执行代码来克服。不幸的是,如果实际目标不是依赖于用户启动的 activity,这不是解决方法。

这就是我认为自己迷路的地方。但是,上述规则也有一些例外,BOOT_COMPLETED 属于这种例外。令人惊讶的是,correct documentation page 被称为"Implicit Broadcast Exceptions",更令人惊讶的是,它不是很好找。无论如何,它说

ACTION_LOCKED_BOOT_COMPLETED, ACTION_BOOT_COMPLETED

Exempted because these broadcasts are only sent only once, at first boot, and many apps need to receive this broadcast to schedule jobs, alarms, and so forth.

这正是这里所需要的,并且已被 Google 注意到。总结一下:是的,大多数个隐式广播接收器已经被放弃了,但是不是所有BOOT_COMPLETED就是其中之一。它仍然有效,并且(希望)将来会有效。

第二个问题仍然悬而未决:如果用户从不重启设备并且从不在安装后至少启动一次应用程序,则 WatchDogServer 永远不会入队。 (这是我对问题的解决方案中遗漏的 5%。)有一个 ACTION_PACKAGE_ADDED 意图,但它在这里没有帮助,因为已添加的特定应用程序从未收到其 "own" 意图.

无论如何,上述缺点无法克服,它是 Google 反恶意软件活动的一部分。 (不幸的是,我丢失了 link 的参考。)这是一个实用的解决方案,可以阻止恶意软件静默建立后台任务。安装包后,它会保持某种 "semi-installed" 状态。 (它被Google称为"paused",但不要将它与activity的暂停状态混淆。这里指的是整个包的状态。)包保持此状态,直到用户至少从启动器使用 android.intent.action.MAIN-intent 手动启动主 activity 一次。只要包处于 "paused" 状态,它就不会收到任何广播意图。在这种特殊情况下,下次启动时不会收到 BOOT_COMPLETED-intent。总结一下:您不能编写仅包含后台任务的应用程序,即使这是您应用程序的全部目的。您的应用需要至少向用户显示一次的 activity。否则什么都不会运行。巧合的是,由于法律原因,大多数国家/地区的大多数应用程序无论如何都需要某种法律说明或数据政策,因此您可以使用 activity 静态显示。在 Playstore 的应用描述中要求用户启动应用(甚至可能阅读您的文字)以完成安装。