若要手机自动执行任务,首要做的就是保活自己的app,避免被系统把进程杀掉。
网络上有许多app保活方式,也有许多靠谱的操作,更多的则是不寻常的路子,而且还不稳定,容易被系统回收资源。
下面列出三种自己探索过的app保活方式
方案一:通知保活
在通知栏创建一个常驻通知,创建的时候将自己应用的上下文传入,目前个中音乐app就是用这种方式实现长时间驻留后台的,Google官方也推荐这种方式。
定义执行任务的服务,在服务开启的时候显示通知栏的通知保活
点我展开
import android.app.*
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.util.Log
import cn.ljt.clock.open
import cn.ljt.clock.playAudio
import com.tencent.mmkv.MMKV
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.fixedRateTimer
class NotificationService : Service() {
val channelId: String = "ChannelId"
lateinit var timer: Timer
override fun onCreate() {
super.onCreate()
startTimer()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
packageName + "打卡",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
startForeground(1, getNotification())
return START_STICKY
}
private fun getNotification(): Notification {
val contentIntent = Intent()
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
contentIntent.action = "android.settings.APPLICATION_DETAILS_SETTINGS"
contentIntent.data = Uri.fromParts("package", packageName, null)
val builder: Notification.Builder = Notification.Builder(this)
.setSmallIcon(android.R.mipmap.sym_def_app_icon)
.setContentIntent(PendingIntent.getActivity(this, 0, contentIntent, 0))
.setContentTitle("\"$packageName\"正在运行")
.setContentText("触摸即可了解详情或停止应用")
//设置Notification的ChannelID,否则不能正常显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(channelId)
}
return builder.build()
}
override fun onDestroy() {
super.onDestroy()
stopTimer()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
stopForeground(true)
}
private fun startTimer() {
timer = fixedRateTimer("", false, 0, period * 1000) {
try {
// TODO: 根据条件执行任务
} catch (e: Exception) {
Log.i("TAG", "$e")
}
}
}
private fun stopTimer() {
timer.cancel()
Log.d("TAG", "stopTimer")
}
}
方案二:闹钟唤醒
实现方式时到点发送一个PendingIntent
,自己应用声明一个对应的receiver,在receiver中处理事件。
-
AndroidManifest.xml
文件添加receiver
点我展开
<receiver
android:name=".ui.alarm.AlarmReceiver"
android:enabled="true"
android:exported="true" />
- 定义receiver类
点我展开
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action.equals("${context.packageName}.alarm")) {
try {
// TODO: 根据条件执行任务
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
- 定义闹钟并开启
点我展开
val intent = Intent("$packageName.alarm")
intent.setClass(this, AlarmReceiver::class.java)
val pi = PendingIntent.getBroadcast(this, 0, intent, 0) //设置一个PendingIntent对象,发送广播
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager //获取AlarmManager对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
val nextAlarmClock = alarmManager.nextAlarmClock
if (nextAlarmClock != null) {
val showIntent = nextAlarmClock.showIntent
val triggerTime = nextAlarmClock.triggerTime
Log.i("TAG", "init: $showIntent")
Log.i("TAG", "init: $triggerTime")
}
}
//AlarmManager.ELAPSED_REALTIME: 闹钟在手机睡眠状态下不可用,该状态下闹钟使用相对时间(相对于系统启动开始),状态值为3;
//AlarmManager.ELAPSED_REALTIME_WAKEUP: 闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间,状态值为2;
//AlarmManager.RTC: 闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间,状态值为1;
//AlarmManager.RTC_WAKEUP: 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间,状态值为0;
//AlarmManager.POWER_OFF_WAKEUP: 表示闹钟在手机关机状态下也能正常进行提示功能,所以是5个状态中用的最多的状态之一,该状态下闹钟也是用绝对时间,状态值为4;不过本状态好像受SDK版本影响,某些版本并不支持;
// 重复执行,倒数第二参数为间隔时间,单位为毫秒
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, intervalMillis * 1000, pi)
//时间到时,执行PendingIntent,只执行一次
alarmManager[AlarmManager.RTC_WAKEUP, calendar.timeInMillis] = pi
方案三:屏保保活
系统的屏保是一个特殊的service,可以设置屏保的UI,在屏保显示的过程中,app一直存活。同样屏保消失的时候,service也会被销毁。
-
AndroidManifest.xml
中添加屏保服务,一定要添加android.permission.BIND_DREAM_SERVICE
权限
点我展开
<service
android:name=".service.MyDream"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.service.dream"
android:resource="@xml/my_dream" />
</service>
- meta-data指向的资源文件里面声明了屏保的设置页面
点我展开
<?xml version="1.0" encoding="utf-8"?>
<dream xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="cn.ljt.clock.ui.dreamsetting.MyDreamActivity" />
- 屏保服务对应的实体类,继承
DreamService
,并设置屏保的UI
点我展开
import android.content.Context
import android.os.SystemClock
import android.service.dreams.DreamService
import android.util.Log
import cn.ljt.clock.R
import com.tencent.mmkv.MMKV
import java.util.*
import kotlin.concurrent.fixedRateTimer
class MyDream : DreamService() {
private val TAG: String? = MyDream::class.java.name
var boolean: Boolean? = false
lateinit var timer: Timer
/**
* 初始化设置,在这里可以调用 setContentView()
*/
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.i(TAG, "onAttachedToWindow: ")
// Exit dream upon user touch
isInteractive = false
// Hide system UI
isFullscreen = true
//设置为false会降低屏幕亮度
isScreenBright = false
// Set the dream layout
setContentView(R.layout.my_day_dream)
boolean = MMKV.defaultMMKV()?.getBoolean("dream_task_enable", false)
}
/**
* 在这里回收前面调用的资源(比如 handlers 和 listeners)
*/
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.i(TAG, "onDetachedFromWindow: ")
}
/**
* 互动屏保已经启动,这里可以开始播放动画或者其他操作
*/
override fun onDreamingStarted() {
super.onDreamingStarted()
Log.i(TAG, "onDreamingStarted: ")
if (boolean == true) {
startTimer()
}
}
/**
* 在停止 onDreamingStarted() 里启动的东西
*/
override fun onDreamingStopped() {
super.onDreamingStopped()
Log.i(TAG, "onDreamingStopped: ")
if (boolean == true) {
stopTimer()
}
}
private fun startTimer() {
timer = fixedRateTimer("fixedRateTimer", true, period * 1000, period * 1000) {
try {
// TODO: 根据条件执行任务
} catch (e: Exception) {
Log.i("TAG", "$e")
}
}
}
private fun stopTimer() {
timer.cancel()
Log.d("TAG", "stopTimer")
}
}
避开屏幕锁
以上方法可以保活,但是屏幕锁定之后无法开锁。
尝试多种方式,在高版本的手机上,依然无法自动解锁。
在上述三种方案中,第三种屏保的方式可以阻止手机进入锁屏状态,故从它入手。
正常状态下,屏保开启后,轻触屏幕,屏保会自动退出并点亮屏幕。
所以,只需执行代码,模拟轻触屏保的动作即可。
以下提供三种方式,在屏保开启后,试图唤醒页面
- 调用
View
的performClick()
方法(失败) - 调用
DreamService
的wakeUp()
方法(失败) - 模拟屏幕点击(成功)
点我展开
private fun click() {
val inst = Instrumentation()
inst.sendPointerSync(
MotionEvent.obtain(
SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_DOWN,
240f,
400f,
0
)
)
inst.sendPointerSync(
MotionEvent.obtain(
SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_UP,
240f,
400f,
0
)
)
}