Android12及之后定时任务的实现——类似暗色模式定时开关
在最近拿到了一个需求需要实现暗色模式的定时开关,最初选择用AlarmManager的setRepeating方法进行实现。setRepeating这个方法在安卓12以下有一个参数是可以控制定时任务的开启时间的,但是由于大版本升级,在Android12上对电池进行了一些优化,再继续使用这个方法的话只能定一个而且不是精确定时。所以就自己琢磨了一个循环定时任务的实现,具体如下
任务的开启:
这里主要是使用Calendar进行时间设定后启动一个实现具体功能的Receiver(如:暗色模式的开关)。在这里需要传入三个值,为了演示方便,我这里只使用id,时间为获取当前时间+10s,在具体的实现中这里还需要将设定的时间传进来(这里的时间需要使用24小时制)
- id:如果有多个时间来启动该任务,这个id需要动态传入,每个id对应一个时间
- hour:任务开启的小时
- minute:任务开启的分钟
主activity
@SuppressLint("ScheduleExactAlarm")
private void buttonDown(int id){
//在需要定规定的时间的时候放开下列代码进行时间的设置,并将buttonDown(int id)传入的参数修改为buttonDown(int hour ; int minute ;int id)
//hour:所设定的小时;minute:所设定的分钟;id:多个时间重启该任务进行区分的标识位;
// Calendar c = Calendar.getInstance();
// c.set(Calendar.HOUR_OF_DAY,hour);
// c.set(Calendar.MINUTE,minute);
// c.set(Calendar.SECOND,0);
// c.set(Calendar.MILLISECOND,0);
//设置时间为当前之前则天数+1
// if (c.getTimeInMillis()<System.currentTimeMillis()){
// c.add(Calendar.DAY_OF_YEAR,1);
// }
Log.d(TAG, "任务开启");
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, ButtonDownReceiver.class);
//这个传入的id是用来在receiver中对同一个任务的不同实现来进行区分的。例如:暗色模式的开/关
intent.putExtra("operator",id);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, id, intent, PendingIntent.FLAG_IMMUTABLE);
//这里负责启动定时任务,具体任务内容实现在DarkClockIntentReceiver中。
//在具体的实现中,System.currentTimeMillis()+(1000*10)需要替换为c.getTimeInMillis()方法来获取所设定的时间的时间戳
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(1000*10), pendingIntent);
}
ButtonDownReceiver.java
这里的flag就是用来判别 开启的时间点实现哪一个逻辑(例如:暗色模式的开/关)。onReceive方法只进行判别,具体要实现的功能放在buttonOperation方法中。
这里实现任务循环执行的方式是:获取当前时间后加上循环的间隔(比如说每日循环就给当前时间加上个【10006060*24】让24小时后再执行一遍)。说简单点就是手动搞了一个死循环在这块。
注:这里是在主线程上跑的,所以不要去进行一些耗时的操作,不然容易引起应用ANR
@Override
public void onReceive(Context context, Intent intent) {
// 当接收到闹钟触发的 Intent 时执行
Log.d(TAG, "进入广播");
int flag = intent.getIntExtra("operator",-1);
Log.e(TAG, "flag + "+flag );
switch (flag){
case 1:
Log.e(TAG, "开始定时");
buttonOperation(context);
break;
case 2:
break;
default:
break;
}
long time = System.currentTimeMillis()+(1000*10);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intents = new Intent(context, ButtonDownReceiver.class);
intents.putExtra("operator",flag);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, flag, intents, PendingIntent.FLAG_IMMUTABLE);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,time , pendingIntent);
}
private void buttonOperation(Context context) {
Log.e(TAG, "开始运行具体逻辑");
Toast.makeText(context, "开始运行", Toast.LENGTH_SHORT).show();
}
任务的关闭
要关闭任务的话是很简单的,直接给AlarmManager停了就行。这里传的id切记要和上面保持一致,不然的话是关不掉的。
AlarmManager关闭单个任务是通过pendingIntent来判定的,所以一定要保证在开启、关闭、重设的时候用的是同一个pendingIntent。如果发现关闭不生效,先检查一下这里是不是实例化的有问题。
private void stopButton(int id){
Log.d(TAG, "任务取消");
//实现定时任务的停止,主要是使用alarmManager.cancel()方法,方法这里的参数为前面设定的pendingIntent
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(MainActivity.this, ButtonDownReceiver.class);
intent.putExtra("operator",id);
//这里重设的pendingInteng与开启定时任务的那个要完全一致,否则无法关闭定时任务
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,id,intent,PendingIntent.FLAG_IMMUTABLE);
alarmManager.cancel(pendingIntent);
}
任务的重启
关闭后在某些场景下肯定不能是想再开的时候去给重设一下而且上面的那些逻辑如果系统重启了也就失效了需要重设,所以这里还搞了一个重启的逻辑在这里。
重启实现起来也很简单,注册一个本地广播然后在广播里面搞一下重启就好了
主activity
这块实现的是手动重启。
注:本地广播是动态注册的,记得销毁
private IntentFilter intentFilter;
private ReBootReceiver reBootReceiver;
private LocalBroadcastManager localBroadcastManager;
Button mButton3 = findViewById(R.id.mButton3);
mButton3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//这里的重启逻辑使用本地广播在另一个receiver中对时间进行重新设定
Intent intent = new Intent("com.zimo.test.REBOOT_BUTTON");
localBroadcastManager.sendBroadcast(intent);
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.zimo.test.REBOOT_BUTTON");
reBootReceiver = new ReBootReceiver();
localBroadcastManager.registerReceiver(reBootReceiver,intentFilter);//注册本地广播监听器
ReBootReceiver.java
这块重新设定时间和首次设定时间的时候是一样的,和首次设定的区别就是如果你开启了好几个不同的定时任务,那就去把每个定时任务都重启一下。
这里具体重启的时间是通过广播控制的,一个是开机广播,让每次开机的时候都重新设定一下时间,一个是手动的去点一下进行重启。
public class ReBootReceiver extends BroadcastReceiver {
private static final String TAG = "ReBootReceiver";
private Context mContext;
//如果有多个设定的时间需要重启,在这里继续写标识位。
private static final int REBOOT_BUTTON_1 = 1;
@Override
public void onReceive(Context context, Intent intent) {
mContext = context;
String action = intent.getAction();
if (action.equals(Intent.ACTION_BOOT_COMPLETED)||action.equals("com.zimo.test.REBOOT_BUTTON")){
//这里接受开机广播和重设时按下按钮后发出的广播来对时间进行重设
//同样的,在实际开发过程中这里也需要传设定的时间进去后进行设置,这里用系统时间+5s代替
reBootButton(REBOOT_BUTTON_1);
}
}
@SuppressLint("ScheduleExactAlarm")
private void reBootButton(int id){
// Calendar c = Calendar.getInstance();
// c.set(Calendar.HOUR_OF_DAY,hour);
// c.set(Calendar.MINUTE,minute);
// c.set(Calendar.SECOND,0);
// c.set(Calendar.MILLISECOND,0);
//设置时间为当前之前则天数+1
// if (c.getTimeInMillis()<System.currentTimeMillis()){
// c.add(Calendar.DAY_OF_YEAR,1);
// }
Log.d(TAG, "任务重启");
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(mContext, ButtonDownReceiver.class);
intent.putExtra("operator",id);
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, id, intent, PendingIntent.FLAG_IMMUTABLE);
//这里负责启动定时任务,具体任务内容实现在DarkClockIntentReceiver中。
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(1000*10), pendingIntent);
}
}
需要注意的点
上面我的例子都是点击开启的十秒后开启然后每隔十秒一循环,同时没有去传定时的时间值,如果是需要在某一个固定时间进行什么操作的话,每个传“id”值的地方都把时间传进去。这套逻辑删删改改就是个能实现每日循环的闹钟
但是这块还是有一点不足,就是如果应用被杀后台之后这块就失效了。
完整代码
MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Context mContext ;
private IntentFilter intentFilter;
private ReBootReceiver reBootReceiver;
private LocalBroadcastManager localBroadcastManager;
@SuppressLint("ScheduleExactAlarm")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
setContentView(R.layout.activity_main);
localBroadcastManager = localBroadcastManager.getInstance(this);
}
@Override
protected void onResume() {
super.onResume();
//Button1实现首次定时以及后续的循环
Button mButton1 = findViewById(R.id.mButton1);
mButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//传值这里我使用button来进行逻辑的演示,在代码中的具体实现中大概率这里用的是ListView,这里的id值可以用listview的下标来控制或者单独写个常量来进行控制
//另外,由于做的是定时任务,这里还需要将所定下的时间传到方法里面,具体需要什么在下面的方法里。
buttonDown(1);
}
});
//button2实现的是对定时的关闭
Button mButton2 = findViewById(R.id.mButton2);
mButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//传入的id为定时任务的标识位,如果有多个时间开启任务,这里需要多次传值
stopButton(1);
}
});
//button3实现的是对定时的重启以及时间的重新设置
Button mButton3 = findViewById(R.id.mButton3);
mButton3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//这里的重启逻辑使用本地广播在另一个receiver中对时间进行重新设定
Intent intent = new Intent("com.zimo.test.REBOOT_BUTTON");
localBroadcastManager.sendBroadcast(intent);
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.zimo.test.REBOOT_BUTTON");
reBootReceiver = new ReBootReceiver();
localBroadcastManager.registerReceiver(reBootReceiver,intentFilter);//注册本地广播监听器
}
@SuppressLint("ScheduleExactAlarm")
private void buttonDown(int id){
//在需要定规定的时间的时候放开下列代码进行时间的设置,并将buttonDown(int id)传入的参数修改为buttonDown(int hour ; int minute ;int id)
//hour:所设定的小时;minute:所设定的分钟;id:多个时间重启该任务进行区分的标识位;
// Calendar c = Calendar.getInstance();
// c.set(Calendar.HOUR_OF_DAY,hour);
// c.set(Calendar.MINUTE,minute);
// c.set(Calendar.SECOND,0);
// c.set(Calendar.MILLISECOND,0);
//设置时间为当前之前则天数+1
// if (c.getTimeInMillis()<System.currentTimeMillis()){
// c.add(Calendar.DAY_OF_YEAR,1);
// }
Log.d(TAG, "任务开启");
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(this, ButtonDownReceiver.class);
intent.putExtra("operator",id);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, id, intent, PendingIntent.FLAG_IMMUTABLE);
//这里负责启动定时任务,具体任务内容实现在DarkClockIntentReceiver中。
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(1000*10), pendingIntent);
}
@Override
protected void onDestroy() {
//动态注册的广播记得销毁
localBroadcastManager.unregisterReceiver(reBootReceiver);
super.onDestroy();
}
ButtonDownReceiver
public class ButtonDownReceiver extends BroadcastReceiver{
private static final String TAG = "ButtonDownReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// 当接收到闹钟触发的 Intent 时执行
Log.d(TAG, "进入广播");
int flag = intent.getIntExtra("operator",-1);
Log.e(TAG, "flag + "+flag );
switch (flag){
case 1:
Log.e(TAG, "开始定时");
buttonOperation(context);
break;
case 2:
break;
default:
break;
}
long time = System.currentTimeMillis()+(1000*10);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intents = new Intent(context, ButtonDownReceiver.class);
intents.putExtra("operator",flag);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, flag, intents, PendingIntent.FLAG_IMMUTABLE);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,time , pendingIntent);
}
private void buttonOperation(Context context) {
Log.e(TAG, "开始运行具体逻辑");
Toast.makeText(context, "开始运行", Toast.LENGTH_SHORT).show();
}
}
ReBootReceiver
public class ReBootReceiver extends BroadcastReceiver {
private static final String TAG = "ReBootReceiver";
private Context mContext;
//如果有多个设定的时间需要重启,在这里继续写标识位。
private static final int REBOOT_BUTTON_1 = 1;
@Override
public void onReceive(Context context, Intent intent) {
mContext = context;
String action = intent.getAction();
if (action.equals(Intent.ACTION_BOOT_COMPLETED)||action.equals("com.zimo.test.REBOOT_BUTTON")){
//这里接受开机广播和重设时按下按钮后发出的广播来对时间进行重设
//同样的,在实际开发过程中这里也需要传设定的时间进去后进行设置,这里用系统时间+5s代替
reBootButton(REBOOT_BUTTON_1);
}
}
@SuppressLint("ScheduleExactAlarm")
private void reBootButton(int id){
// Calendar c = Calendar.getInstance();
// c.set(Calendar.HOUR_OF_DAY,hour);
// c.set(Calendar.MINUTE,minute);
// c.set(Calendar.SECOND,0);
// c.set(Calendar.MILLISECOND,0);
//设置时间为当前之前则天数+1
// if (c.getTimeInMillis()<System.currentTimeMillis()){
// c.add(Calendar.DAY_OF_YEAR,1);
// }
Log.d(TAG, "任务重启");
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(mContext, ButtonDownReceiver.class);
intent.putExtra("operator",id);
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, id, intent, PendingIntent.FLAG_IMMUTABLE);
//这里负责启动定时任务,具体任务内容实现在DarkClockIntentReceiver中。
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(1000*10), pendingIntent);
}
}
问题补充
上面的代码在实测过程中发现,当系统时间改变后(手动修改系统时间/网络重新同步系统时间),如果改变后的系统时间的时间戳大于当前时间的时间戳时alarmManager会立刻触发。这里是因为alarmManager在修改系统时间后会去检测当前已经设定的闹钟,如果当前时间大于设定的时间就会去立刻触发alarmManager。
至于这个问题的修复方案,我在这里使用的是Intent.ACTION_TIME_CHANGED和Intent.ACTION_DATE_CHANGED两个广播。
具体的实现就是在ReBootReceiver的onReceive方法中去监听这两个广播,监听到了之后先.cancel掉当前设定的闹钟再根据当前的时间戳以及预定时间去重设一下就好了。
这俩广播在android8之后就不能静态注册了,至于动态注册的位置,那当然是修改系统时间的类里面喽。