The Correct Approach to Android App Persistence: Avoiding the Trap of Endless Persistence Requirements

Before we begin, let me briefly introduce some previous "black technologies":
About 6 years ago, a library called MarsDaemon appeared on Github, which achieved persistence through a dual-process guardian approach, becoming quite popular for a time. However, this was short-lived, as it became obsolete with the arrival of Android 8.0.
In the last two years, a new library called Leoric has emerged on Github; feel free to check out the source code. Who would dare to use it in a production environment? Only those who are just experimenting would dare to use it (we can't let persistence lead to a sluggish phone). I haven't tried it, but I want to say: can black technology last forever?
Without rules, nothing can be accomplished. To improve product survival rates, we ultimately need to focus on the product itself, respect users, and enhance user experience—that is the right path.
I have also suffered from the pressure of persistence requirements. Recently, I noticed someone in a QQ group mentioned how to achieve persistence, so let's discuss how to correctly keep an App alive?

After Android 8.0: Background restrictions have been strengthened. At that time, I tested a set of data:
When an app is in the foreground, starting a foreground Service that uses JobScheduler to trigger a scheduled task (every 30 seconds), and the phone is locked, the scheduled task executes normally for the first 10 minutes;
After about 12 minutes, I found that the app process was killed, and when I unlocked the screen, the app was no longer in the foreground;
Major domestic smartphone manufacturers have modified their systems, each with their own self-start management. Xiaomi phones are even more chaotic (there was a concept of "stealth mode" at that time, which was a master at killing background processes). It can only be said that Android phones at that time lacked performance in various aspects. Each manufacturer had its own power-saving mode to achieve battery savings and improve phone performance. The Android system has become increasingly refined, but the manufacturers' custom self-start and power-saving modes still exist, so we need to ensure persistence.

1. Before Android 8.0 - Common persistence solutions:
   1. Start a foreground Service.
   2. Android 6.0+ Ignore battery optimization switch (code will follow).
   3. Accessibility service (only applicable to apps that utilize this feature, such as Alipay voice enhancement reminders).

2. After Android 8.0 - Common persistence solutions:
   1. Start a foreground Service (can be added, but alone cannot meet persistence requirements).
   2. Android 6.0+ Ignore battery optimization switch (code will follow).
   3. Accessibility service (only applicable to apps that utilize this feature, such as Alipay voice enhancement reminders).
   4. Application self-start permission (the simplest solution is to provide tutorial images for different systems—allowing users to open it themselves).
   5. Lock the multi-task list window (provide GIF tutorial images—allowing users to open it themselves).
   6. Hide App in the multi-task list window (only for apps with this requirement).
   7. High power consumption in the background (only for Vivo phones).

3. Steps to implement persistence solutions:
(1) Foreground Service // Foreground service
class ForegroundCoreService : Service() {
    override fun onBind(intent: Intent?): IBinder? = null
    private var mForegroundNF:ForegroundNF by lazy {
        ForegroundNF(this)
    }
    override fun onCreate() {
        super.onCreate()
        mForegroundNF.startForegroundNotification()
    }
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if(null == intent){
            // Service restarted after being killed by the system
            return START_NOT_STICKY
        }
        mForegroundNF.startForegroundNotification()
        return super.onStartCommand(intent, flags, startId)
    }
    override fun onDestroy() {
        mForegroundNF.stopForegroundNotification()
        super.onDestroy()
    }
}
// Initialize foreground notification, stop foreground notification
class ForegroundNF(private val service: ForegroundCoreService) : ContextWrapper(service) {
    companion object {
        private const val START_ID = 101
        private const val CHANNEL_ID = "app_foreground_service"
        private const val CHANNEL_NAME = "Foreground Persistence Service"
    }
    private var mNotificationManager: NotificationManager? = null
    private var mCompatBuilder:NotificationCompat.Builder?=null
    private val compatBuilder: NotificationCompat.Builder?
            get() {
                if (mCompatBuilder == null) {
                    val notificationIntent = Intent(this, MainActivity::class.java)
                    notificationIntent.action = Intent.ACTION_MAIN
                    notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
                    notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                    // Action intent
                    val pendingIntent = PendingIntent.getActivity(
                        this, (Math.random() * 10 + 10).toInt(),
                        notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT
                    )
                    val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this,CHANNEL_ID)
                    // Title
                    notificationBuilder.setContentTitle(getString(R.string.notification_content))
                    // Notification content
                    notificationBuilder.setContentText(getString(R.string.notification_sub_content))
                    // Small icon displayed in the status bar
                    notificationBuilder.setSmallIcon(R.mipmap.ic_coolback_launcher)
                    // Intent to open notification content
                    notificationBuilder.setContentIntent(pendingIntent)
                    mCompatBuilder = notificationBuilder
                }
                return mCompatBuilder
            }
    init {
        createNotificationChannel()
    }
    // Create notification channel
    private fun createNotificationChannel() {
        mNotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        // For systems 8.0+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                NotificationManager.IMPORTANCE_LOW
            )
            channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
            channel.setShowBadge(false)
            mNotificationManager?.createNotificationChannel(channel)
        }
    }
    // Start foreground notification
    fun startForegroundNotification() {
        service.startForeground(START_ID, compatBuilder?.build())
    }
    // Stop foreground service and clear notification
    fun stopForegroundNotification() {
        mNotificationManager?.cancelAll()
        service.stopForeground(true)
    }
}

(2) Ignore battery optimization (Android 6.0+)
1. We need to declare permission in AndroidManifest.xml:
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
2. Request permission to ignore battery optimization through Intent (needs to guide the user to click):
// Register ActivityResult in the Activity's onCreate, must register in onCreate
// Listen to onActivityForResult callback
mIgnoreBatteryResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
            // Check if it was successfully enabled
            if(queryBatteryOptimizeStatus()){ 
               // Ignore battery optimization successfully enabled
            }else{
               // Failed to enable
            }
        }
Open the ignore battery optimization dialog through Intent:
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
// Start ignore battery optimization, a system dialog will pop up
launchActivityResult(intent)
Check if the ignore battery optimization switch was successfully enabled:
fun Context.queryBatteryOptimizeStatus():Boolean{
    val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager?
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        powerManager?.isIgnoringBatteryOptimizations(packageName)?:false
    } else {
        true
    }
}

(3) Accessibility service
Refer to the official documentation: Create your own accessibility service. It is also a Service with a higher priority, providing interface enhancement functions, originally intended to assist users with visual impairments or those who may temporarily be unable to interact fully with the device.
It can do many things; using this Service, there is no need to request overlay permission in 6.0+; you can directly use WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, which is quite convenient (only for apps that need this service, can enhance background persistence).

(4) Self-start permission (i.e., whitelist management list page) is the system's entry point for users to open the "self-start permission" switch. We need to pop up prompts for different smartphone manufacturers and system versions to guide users to open the "self-start permission".
Some manufacturers call it: whitelist management, others call it: self-start permission; both are the same concept;
(Note: If controlled by code, it cannot be guaranteed to always adjust, system upgrades may block it. The simplest method is to show a guide image on how to find the self-start page; below is an example for Huawei phones:)
Huawei phone - self-start management

(5) Locking the multi-task list window can guide users to open the App window lock so that clicking clean/accelerate won't kill the application, showing tutorial images for different manufacturers.
Huawei phone window lock - tutorial image

(6) Hiding App in the multi-task list window, after locking the multi-task window, prompt the user to open the switch to hide App in the App settings, so users won't accidentally swipe away the App window in the multi-task list.
Hiding App window in the multi-task list can be controlled with the following code (this is just an enhanced solution for apps with this requirement; because the window is hidden, users won't think about it and won't accidentally swipe it away):
// Hide App window in the multi-task list
fun hideAppWindow(context: Context,isHide:Boolean){
        try {
            val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
            // Control whether the App window is displayed in the multi-task list
            activityManager.appTasks[0].setExcludeFromRecents(isHide)
        }catch (e:Exception){
            .....
        }
    }

(7) High power consumption in the background (specific to Vivo phones)
Entry point: "Settings">"Battery">"High power consumption in background">"Find xxxApp and turn on the switch"
vivo allows high power consumption in the background

Finally, I advise those developers who are still obsessed with finding black technologies to wake up; the sun is shining on your back.
If your App user base is not ordinary users but specialized for some tech enthusiasts who can root their phones, then simply move it to the system directory priv/system/app, and even if users force kill it, it will automatically relaunch.


Transcribed from: Juejin Halifax https://juejin.cn/post/7003992225575075876#heading-9

PS: If you find my sharing helpful, feel free to like, share, and follow.

PS:Feel free to leave your opinions in the comments section for discussion on improvements. If today’s article has inspired you, please share it with more people.

Leave a Comment

×