The original link: segmentfault.com/a/119000002…

Recently, I am working on a buried SDK. Since buried points are uploaded in batches, not every time, there will be a mechanism to keep the process alive. This is also one of the implementation technologies of self-research push: how to ensure the survival of Android processes.

For Android, there are mainly some ways to keep alive:

  • Enable foreground Service (good effect, recommended)
  • Play a silent audio loop in Service (good effect, but high power consumption, use with caution)
  • Dual-process daemon (available before Android 5.0)
  • JobScheduler (introduced after Android 5.0, invalid after Android 8.0) 1-pixel Activity keepalive solution (not recommended) Broadcast screen lock, custom screen lock (not recommended) Third-party push SDK wake up (good effect, disadvantages are third-party access) The following is the implementation scheme:

1. Listen to the lock screen broadcast and start the Activity at 1 pixel

This scheme was first seen in 2015. In order to show investors monthly activities, an FM app started a 1-pixel Activity in the Android app.

Since the level of the Activity is relatively high, starting the Activity at 1 pixel ensures that the process cannot be killed easily.

Specifically, define a 1-pixel Activity that dynamically registers custom broadcasts.

class OnePixelActivity : AppCompatActivity() { private lateinit var br: BroadcastReceiver override fun onCreate(savedInstanceState: Bundle?) {super.oncreate (savedInstanceState) // Set the activity to one pixel val window = window windowed.setgravity (Gravity.LEFT or Gravity.TOP)  val params = window.attributes params.x = 0 params.y = 0 params.height = 1 params.width = 1 window.attributes = params // Register the broadcast receiver in the 1-pixel activity to receive a pixel until the broadcast endsBroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                finish()
            }
        }
        registerReceiver(br, IntentFilter("finish activity"))
        checkScreenOn()
    }

    override fun onResume() {
        super.onResume()
        checkScreenOn()
    }

    override fun onDestroy() {try {// unregisterReceiver(br)} Catch (e: IllegalArgumentException) {} super.ondestroy ()} /** * Check if the screen is lit */ private FuncheckScreenOn() {
        val pm = [email protected](Context.POWER_SERVICE) as PowerManager
        val isScreenOn = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            pm.isInteractive
        } else {
            pm.isScreenOn
        }
        if (isScreenOn) {
            finish()
        }
    }
}
Copy the code

2. Two-process daemon

Dual-process daemons are available until Android 5.0, but not after. First, we define a local service that plays silent music and binds the remote service

class LocalService : Service() {
    private var mediaPlayer: MediaPlayer? = null
    private var mBilder: MyBilder? = null

    override fun onCreate() {
        super.onCreate()
        if (mBilder == null) {
            mBilder = MyBilder()
        }
    }

    override fun onBind(intent: Intent): IBinder? {
        returnMBilder} override fun onStartCommand(intent: intent, flags: Int, startId: Int): Int {// Play silent musicif(mediaPlayer == null) {mediaPlayer = mediaPlayer. create(this, r.raw.novioce) // Sound set to 0 mediaPlayer? .setVolume(0f, 0f) mediaPlayer? .isLooping =true} // Enable the foreground service to increase the priorityif(KeepLive.foregroundNotification ! = null) { val intent2 = Intent(applicationContext, NotificationClickReceiver::class.java) intent2.action = NotificationClickReceiver.CLICK_NOTIFICATION val notification = NotificationUtils.createNotification(this, KeepLive.foregroundNotification!! .getTitle(), KeepLive.foregroundNotification!! .getDescription(), KeepLive.foregroundNotification!! .geticonres (), intent2) startForeground(13691, notification)}.geticonres (), intent2) startForeground(13691, notification)} RemoteService::class.java) this.bindService(intent3, connection, Context.BIND_ABOVE_CLIENT) } catch (e: Exception) {} // Hide service notification try {if (Build.VERSION.SDK_INT < 25) {
                startService(Intent(this, HideForegroundService::class.java))
            }
        } catch (e: Exception) {
        }

        if(KeepLive.keepLiveService ! = null) { KeepLive.keepLiveService!! .onWorking() }return Service.START_STICKY
    }

    private fun play() {
        if(mediaPlayer ! = null &amp; &amp; ! mediaPlayer!! .isPlaying) { mediaPlayer? .start() } } private inner class MyBilder : GuardAidl.Stub() {

        @Throws(RemoteException::class)
        override fun wakeUp(title: String, discription: String, iconRes: Int) {

        }
    }

    private val connection = object : ServiceConnection {

        override fun onServiceDisconnected(name: ComponentName) {
            val remoteService = Intent(this@LocalService,
                    RemoteService::class.java)
            [email protected](remoteService)
            val intent = Intent(this@LocalService, RemoteService::class.java)
            [email protected](intent, this,
                    Context.BIND_ABOVE_CLIENT)
        }

        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            try {
                if(mBilder ! = null &amp; &amp; KeepLive.foregroundNotification ! = null) { val guardAidl = GuardAidl.Stub.asInterface(service) guardAidl.wakeUp(KeepLive.foregroundNotification? .getTitle(), KeepLive.foregroundNotification? .getDescription(), KeepLive.foregroundNotification!! .getIconRes()) } } catch (e: RemoteException) { e.printStackTrace() } } } override funonDestroy() {
        super.onDestroy()
        unbindService(connection)
        if(KeepLive.keepLiveService ! = null) { KeepLive.keepLiveService? .onStop() } } }Copy the code

Then you define a remote service and bind it to the local service.

class RemoteService : Service() {

    private var mBilder: MyBilder? = null

    override fun onCreate() {
        super.onCreate()
        if (mBilder == null) {
            mBilder = MyBilder()
        }
    }

    override fun onBind(intent: Intent): IBinder? {
        return mBilder
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        try {
            this.bindService(Intent(this@RemoteService, LocalService::class.java),
                    connection, Context.BIND_ABOVE_CLIENT)
        } catch (e: Exception) {
        }
        return Service.START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        unbindService(connection)
    }

    private inner class MyBilder : GuardAidl.Stub() {
        @Throws(RemoteException::class)
        override fun wakeUp(title: String, discription: String, iconRes: Int) {
            if(Build.VERSION.SDK_INT < 25) { val intent = Intent(applicationContext, NotificationClickReceiver::class.java) intent.action = NotificationClickReceiver.CLICK_NOTIFICATION val notification = NotificationUtils.createNotification(this@RemoteService, title, discription, iconRes, intent) [email protected](13691, notification) } } } private val connection = object : ServiceConnection { override fun onServiceDisconnected(name: ComponentName) { val remoteService = Intent(this@RemoteService, LocalService::class.java) [email protected](remoteService) [email protected](Intent(this@RemoteService, LocalService::class.java), this, Context.BIND_ABOVE_CLIENT) } override fun onServiceConnected(name: ComponentName, service: IBinder) {}}} / notification bar click broadcast receiver * * * * / class NotificationClickReceiver:BroadcastReceiver() {

    companion object {
        const val CLICK_NOTIFICATION = "CLICK_NOTIFICATION"
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == NotificationClickReceiver.CLICK_NOTIFICATION) {
            if(KeepLive.foregroundNotification ! = null) {if(KeepLive.foregroundNotification!! .getForegroundNotificationClickListener() ! = null) { KeepLive.foregroundNotification!! .getForegroundNotificationClickListener()? .foregroundNotificationClick(context, intent) } } } } }Copy the code

3, JobScheduler

JobScheduler has been added to Android 5.0 to support a special task scheduler mechanism that can be used to implement process survival, but this method is also disabled in Android8.0. First, we define a JobService to enable local and remote services. @SuppressWarnings(value = ["unchecked"."deprecation"])
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class JobHandlerService : JobService() { private var mJobScheduler: JobScheduler? = null override fun onStartCommand(intent: Intent? , flags: Int, startId: Int): Int { var startId = startId startService(this)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
            val builder = JobInfo.Builder(startId++,
                    ComponentName(packageName, JobHandlerService::class.java.name))
            if(Build. VERSION. SDK_INT > = 24) {builder. SetMinimumLatency (JobInfo. DEFAULT_INITIAL_BACKOFF_MILLIS) / / execution of minimum delay time Builder. SetOverrideDeadline (JobInfo. DEFAULT_INITIAL_BACKOFF_MILLIS) / / perform the longest delay time builder.setMinimumLatency(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS) Builder. SetBackoffCriteria (JobInfo DEFAULT_INITIAL_BACKOFF_MILLIS, JobInfo BACKOFF_POLICY_LINEAR) / / linear retry scheme}else{ builder.setPeriodic(JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS) } builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)  builder.setRequiresCharging(true) // When the charger is inserted, execute the task mJobScheduler? .schedule(builder.build()) }return Service.START_STICKY
    }

    private fun startService(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if(KeepLive.foregroundNotification ! = null) { val intent = Intent(applicationContext, NotificationClickReceiver::class.java) intent.action = NotificationClickReceiver.CLICK_NOTIFICATION val notification = NotificationUtils.createNotification(this, KeepLive.foregroundNotification!! .getTitle(), KeepLive.foregroundNotification!! .getDescription(), KeepLive.foregroundNotification!! .geticonres (), intent) startForeground(13691, notification)}localIntents = intents (context, LocalService::class.java) RemoteService::class.java) startService(localIntent)
        startService(guardIntent)
    }

    override fun onStartJob(jobParameters: JobParameters): Boolean {
        if(! isServiceRunning(applicationContext,"com.xiyang51.keeplive.service.LocalService") | |! isServiceRunning(applicationContext,"$packageName:remote")) {
            startService(this)
        }
        return false
    }

    override fun onStopJob(jobParameters: JobParameters): Boolean {
        if(! isServiceRunning(applicationContext,"com.xiyang51.keeplive.service.LocalService") | |! isServiceRunning(applicationContext,"$packageName:remote")) {
            startService(this)
        }
        return false
    }

    private fun isServiceRunning(ctx: Context, className: String): Boolean {
        var isRunning = false
        val activityManager = ctx
                .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val servicesList = activityManager
                .getRunningServices(Integer.MAX_VALUE)
        val l = servicesList.iterator()
        while (l.hasNext()) {
            val si = l.next()
            if (className == si.service.className) {
                isRunning = true}}return isRunning
    }
}
Copy the code

4. Improve the Service priority

Turn on a notification in the onStartCommand() method to increase the priority of the process. Note: Starting with Android 8.0 (API level 26), all notifications must be assigned a channel, and visual and auditory behavior can be set separately for each channel. The user can then modify these Settings in Settings to determine which notifications can be shown or hidden depending on the application.

First, define a notification toolbar class that is compatible with Android 8.0.

class NotificationUtils(context: Context) : ContextWrapper(context) {

    private var manager: NotificationManager? = null
    private var id: String = context.packageName + "51"
    private var name: String = context.packageName
    private var context: Context = context
    private var channel: NotificationChannel? = null

    companion object {
        @SuppressLint("StaticFieldLeak")
        private var notificationUtils: NotificationUtils? = null

        fun createNotification(context: Context, title: String, content: String, icon: Int, intent: Intent): Notification? {
            if (notificationUtils == null) {
                notificationUtils = NotificationUtils(context)
            }
            var notification: Notification? = null
            notification = if(Build.VERSION.SDK_INT >= 26) { notificationUtils? .createNotificationChannel() notificationUtils? .getChannelNotification(title, content, icon, intent)? .build() }else{ notificationUtils? .getNotification_25(title, content, icon, intent)? .build() }return notification
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    fun createNotificationChannel() {
        if(channel == null) { channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_MIN) channel? .enableLights(false) channel? .enableVibration(false) channel? .vibrationPattern = longArrayOf(0) channel? .setSound(null, null) getManager().createNotificationChannel(channel) } } private fun getManager(): NotificationManager {if (manager == null) {
            manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        }
        returnmanager!! } @RequiresApi(api = Build.VERSION_CODES.O) fun getChannelNotification(title: String, content: String, icon: Int, intent: Intent): PendingIntent = //PendingIntent.FLAG_UPDATE_CURRENT = val PendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)return Notification.Builder(context, id)
                .setContentTitle(title)
                .setContentText(content)
                .setSmallIcon(icon)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
    }

    fun getNotification_25(title: String, content: String, icon: Int, intent: Intent): NotificationCompat.Builder {
        val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        return NotificationCompat.Builder(context, id)
                .setContentTitle(title)
                .setContentText(content)
                .setSmallIcon(icon)
                .setAutoCancel(true)
                .setVibrate(longArrayOf(0))
                .setSound(null)
                .setLights(0, 0, 0)
                .setContentIntent(pendingIntent)
    }
}
Copy the code

5. Workmanager method

Workmanager is an API in Android JetPac that you can use to keep your application alive. To use it, we need to rely on the Workmanager library as follows:

Implementation “Android.arch. work: work-Runtime :1.0.0-alpha06” Worker is an abstract class that specifies specific tasks that need to be performed.

public class KeepLiveWork extends Worker {
    private static final String TAG = "KeepLiveWork";

    @NonNull
    @Override
    public WorkerResult doWork() {
        Log.d(TAG, "keep-> doWork: startKeepService"); StartJobService (); // Start the binding service startKeepService();returnWorkerResult.SUCCESS; }}Copy the code

Then, start the keepWork method,

    public void startKeepWork() {
        WorkManager.getInstance().cancelAllWorkByTag(TAG_KEEP_WORK);
        Log.d(TAG, "keep-> dowork startKeepWork");
        OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(KeepLiveWork.class)
                .setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.SECONDS)
                .addTag(TAG_KEEP_WORK)
                .build();
        WorkManager.getInstance().enqueue(oneTimeWorkRequest);

    }Copy the code