Android自动任务探索

若要手机自动执行任务,首要做的就是保活自己的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中处理事件。

  1. AndroidManifest.xml文件添加receiver
点我展开
<receiver
    android:name=".ui.alarm.AlarmReceiver"
    android:enabled="true"
    android:exported="true" />
  1. 定义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()
            }
        }
    }
}
  1. 定义闹钟并开启
点我展开
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也会被销毁。

  1. 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>
  1. 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" />
  1. 屏保服务对应的实体类,继承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")
    }
}

避开屏幕锁

以上方法可以保活,但是屏幕锁定之后无法开锁。

尝试多种方式,在高版本的手机上,依然无法自动解锁。

在上述三种方案中,第三种屏保的方式可以阻止手机进入锁屏状态,故从它入手。

正常状态下,屏保开启后,轻触屏幕,屏保会自动退出并点亮屏幕。

所以,只需执行代码,模拟轻触屏保的动作即可。

以下提供三种方式,在屏保开启后,试图唤醒页面

  • 调用ViewperformClick()方法(失败)
  • 调用DreamServicewakeUp()方法(失败)
  • 模拟屏幕点击(成功)
点我展开
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
        )
    )
}

Android自动任务探索

上一篇:gdb调试方式


下一篇:Java使用db.properties配置数据库连接属性