返回

Android App Widget 后台计时:保持运行的方案

Android

Android App Widget 计时器:后台持续运行方案

开发Android app widget时,经常会遇到计时器需要在后台持续运行的问题。一旦用户关闭或杀死应用程序,计时器就停止更新,这是一个常见痛点。 这篇文章探讨如何让app widget中的计时器在应用程序关闭后依然能够持续运行。

问题分析

问题核心在于,Android应用程序进程一旦被杀死或移出后台任务列表,应用程序的上下文和服务就失去了继续执行的能力。如果计时器直接运行在主线程或者应用程序的组件内部,它们会随进程一起被终止。AlarmManager 本身并不持有应用的生命周期,它只负责按时发送广播事件,如果应用程序进程没有处于活跃状态,自然接收不到这个广播事件。简单地说,App Widget是应用程序在用户主屏幕的延伸展示,不应该直接在Widget组件里做高耗能任务,需要一些更可靠的方式进行后台计时。

解决方案一:前台服务(Foreground Service)

前台服务能够确保应用程序进程保持运行状态,即使应用程序在后台,从而让计时器持续运作。前台服务会展示一个通知给用户,以提示服务正在运行。

步骤:

  1. 创建一个继承自 Service 的类,并实现必要的方法。

    import android.app.Notification;
    import android.app.NotificationChannel;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.app.Service;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Build;
    import android.os.Handler;
    import android.os.IBinder;
    import androidx.core.app.NotificationCompat;
    
    public class TimerService extends Service {
        private static final String CHANNEL_ID = "TimerChannel";
        private Handler handler = new Handler();
        private Runnable timerRunnable;
        private int currentSeconds = 0;
    
        @Override
        public void onCreate() {
            super.onCreate();
            createNotificationChannel();
            startTimer();
        }
    
       @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
         createNotification();
            return START_STICKY;
        }
    
         private void createNotificationChannel() {
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                      "Timer Channel",
                      NotificationManager.IMPORTANCE_DEFAULT);
              NotificationManager manager = getSystemService(NotificationManager.class);
             if(manager != null){
             manager.createNotificationChannel(channel);
            }
         }
         }
    
    
          private void createNotification(){
              Intent intent = new Intent(this, MainActivity.class);
             PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
    
    
          Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                  .setContentTitle("Timer is running")
                  .setSmallIcon(android.R.drawable.ic_dialog_info)
                 .setContentIntent(pendingIntent)
                 .build();
          startForeground(1, notification);
      }
    
       private void startTimer() {
          timerRunnable = new Runnable() {
              @Override
              public void run() {
                   updateWidget(); // 更新App Widget UI
    
                  currentSeconds++;
                 handler.postDelayed(this, 1000); // 每秒更新
    
              }
         };
       handler.post(timerRunnable); //启动
    }
    
    

private void updateWidget(){
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
ComponentName componentName = new ComponentName(this, TimeAppWidget.class);
int [] ids=appWidgetManager.getAppWidgetIds(componentName);
if (ids != null && ids.length > 0) {
for (int id :ids) {
TimeAppWidget.updateAppWidget(this, appWidgetManager, id,String.valueOf(currentSeconds));
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
handler.removeCallbacks(timerRunnable); //停止
}

   @Override
   public IBinder onBind(Intent intent) {
       return null; // 前台服务通常不需要绑定
   }
}
2. 在 AppWidget 的 `updateAppWidget()` 方法中调用  `context.startForegroundService(new Intent(context,TimerService.class))` 启动服务, 确保在应用初始化,应用widget显示到屏幕时都会触发。 

3. 修改你的widget 代码,更新相应的组件文本

 ```java
 public class TimeAppWidget extends AppWidgetProvider {

 static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                         int appWidgetId,String text) {

        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.time_app_widget);
          views.setTextViewText(R.id.appwidget_text, text);
       appWidgetManager.updateAppWidget(appWidgetId, views);
      }

   @Override
   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
   
         for (int appWidgetId : appWidgetIds) {
            String currentTime =  "0";  //widget init text
            updateAppWidget(context, appWidgetManager, appWidgetId,currentTime); //initial render when load app widget

     
            context.startForegroundService(new Intent(context, TimerService.class)); // start service and keep time update even app closed;
           }
    }
 ```
 确保已经添加`TimeAppWidget` 相应的布局`R.layout.time_app_widget`.
4. 在 `AndroidManifest.xml` 文件中声明服务:
 ```xml
     <service android:name=".TimerService"
      android:enabled="true"
       android:foregroundServiceType="shortService"/>

其中, foregroundServiceType="shortService"用于兼容android 14 新的限制要求。

注意事项:

  • 需要权限:android.permission.FOREGROUND_SERVICE (在Android 9 以上) 或者 android.permission.FOREGROUND_SERVICE_LOCATION(对于Android 10或更高版本)。 务必检查这些权限已正确申请。
  • 前台服务通知对用户可见,必须提供清晰的服务。用户也可以通过通知设置禁用服务通知。
  • 对于不必要的计时任务或者更新不应该频繁执行,避免过多的电量消耗,必要时应该考虑停止前台服务。

解决方案二:使用 WorkManager (适用于非精确计时)

如果对于计时的精确度要求不高,或者允许一定的延迟,WorkManager 是一个不错的选择。WorkManager 基于任务队列机制,能够保证即使应用退出也能可靠地执行后台任务。

步骤:

  1. 引入WorkManager 依赖:

    build.gradle文件中加入 implementation "androidx.work:work-runtime:2.8.1" (版本号根据需求更改)

  2. 创建继承 Worker 的类。

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import android.appwidget.AppWidgetManager;
import android.content.ComponentName;

public class TimerWorker extends Worker {
private int currentSeconds = 0;
public TimerWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}

 @NonNull
 @Override
 public Result doWork() {

   updateWidget(); // 更新 App Widget
 
    currentSeconds++;

 return Result.success();
 }

private void updateWidget(){
Context context=getApplicationContext();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName componentName = new ComponentName(context, TimeAppWidget.class);
int [] ids=appWidgetManager.getAppWidgetIds(componentName);
if (ids != null && ids.length > 0) {
for (int id :ids) {
TimeAppWidget.updateAppWidget(context, appWidgetManager, id,String.valueOf(currentSeconds));
}
}
}
}


3. 在AppWidget 的  `onUpdate`  方法中配置 WorkManager,定义任务运行策略。

```java
     
@Override
   public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
   
       for (int appWidgetId : appWidgetIds) {
            String currentTime = "0";  //初始化的文本
           updateAppWidget(context, appWidgetManager, appWidgetId,currentTime); 

          //enqueue recurring task to update the timer with the workmanager

         PeriodicWorkRequest request =
                  new PeriodicWorkRequest.Builder(TimerWorker.class,
                          15, TimeUnit.MINUTES)//Minimum repeat is 15 Minutes, use interval that satisfy your specific need.
                  .build();
  
               WorkManager
                     .getInstance(context)
                  .enqueue(request);


           }
    }

注意事项:

  • WorkManager 提供不同类型的任务配置:一次性任务 (OneTimeWorkRequest) 和周期性任务 (PeriodicWorkRequest),按需求选择。
  • PeriodicWorkRequest 的执行周期需要大于或等于 15 分钟。 WorkManager适合于精度不高的计时或低频更新。
  • 可以通过 WorkManager 的 getWorkInfos 函数监控任务的执行情况,及时排查问题。
  • 可以使用 Constraint来定义任务执行时的条件, 比如网络条件。
  • 为了应对设备在 Doze 模式和应用待机等优化, 应在测试期间观察计时任务的表现, 有可能出现延迟现象。

安全建议

  1. 检查版本兼容性:考虑API兼容性问题,避免在低版本Android上使用高版本的API。

  2. 定期测试: 定期检查,验证方案在不同Android版本和设备上是否正常运行。

  3. 电量优化:在满足需求的情况下,合理降低计时任务的执行频率,避免不必要的耗电。

  4. 处理错误:对执行计时任务过程中可能出现的错误进行妥善处理,例如使用 try-catch 语句进行捕获和处理,同时可以向后台发送错误日志以便排查问题。

总之,要让 app widget 计时器在应用关闭后仍然保持更新,就需要考虑后台运行机制,并采用适合你实际情况的解决方案,合理配置各项参数才能获得理想的用户体验。