Implementation example of Android background startup activity

Time:2021-10-20
catalogue
  • summary
  • Native Android ROM
  • Customized ROM
    • Detect the permission of background pop-up interface
    • Android P background startup permission
    • Android Q background startup permission
  • summary

    summary

    A few days ago, the product put forward a demand to start an activity of our app in the background. With the update of Android version and the unlimited transformation of various ROM manufacturers, many functions that affect the user experience are limited. There is no way. Although they are rogue functions, they use money to eliminate disasters for others, so they open the road of research.

    Native Android ROM

    First of all, I started with the native ROM of Android. According to the official introduction, the restriction of background startup of activity began with Android 10 (API 29). Before that, the native ROM did not have this restriction, so I started an Android 9 (API 28) and 10 (API 29) emulator respectively. I found that the activity can be started directly from the background on API 28, API 29 is limited and cannot be started directly. Referring to the official description of the restrictions on starting an activity from the background, some unrestricted exceptions are given. In addition, the official recommendation is to show the user a notification instead of starting the activity directly, and then process the corresponding logic after the user clicks the notification. You can also add a full screen intent object through setfullscreenintent when setting notification. After testing, this method can start an activity interface from the background on the Android 10 simulator (android.permission.use_full_screen_intent permission is required). The code is as follows:

    
    object NotificationUtils {
        private const val ID = "channel_1"
        private const val NAME = "notification"
    
        private var manager: NotificationManager? = null
    
        private fun getNotificationManagerManager(context: Context): NotificationManager? {
            if (manager == null) {
                manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
            }
            return manager
        }
    
        fun sendNotificationFullScreen(context: Context, title: String?, content: String?) {
            if (Build.VERSION.SDK_INT >= 26) {
                clearAllNotification(context)
                val channel = NotificationChannel(ID, NAME, NotificationManager.IMPORTANCE_HIGH)
                channel.setSound(null, null)
                getNotificationManagerManager(context)?.createNotificationChannel(channel)
                val notification = getChannelNotificationQ(context, title, content)
                getNotificationManagerManager(context)?.notify(1, notification)
            }
        }
    
        private fun clearAllNotification(context: Context) {
            getNotificationManagerManager(context)?.cancelAll()
        }
    
        private fun getChannelNotificationQ(context: Context, title: String?, content: String?): Notification {
            val fullScreenPendingIntent = PendingIntent.getActivity(
                context,
                0,
                DemoActivity.genIntent(context),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            val notificationBuilder = NotificationCompat.Builder(context, ID)
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle(title)
                .setContentText(content)
                .setSound(null)
                .setPriority(NotificationCompat.PRIORITY_MAX)
                .setCategory(Notification.CATEGORY_CALL)
                .setOngoing(true)
                .setFullScreenIntent(fullScreenPendingIntent, true)
            return notificationBuilder.build()
        }
    }
    

    Up to now, the overall feeling is good. At this stage, the Android native ROM can normally start the activity interface from the background. Both Android 9 and 10 versions are happy.

    Customized ROM

    The problem began to surface. Because major manufacturers have different customization of Android, and Android does not inherit the GPL protocol, it uses the Apache open source license agreement, that is, third-party manufacturers can close the source after modifying the code. Therefore, it is impossible to know what changes have been made to the source code of the manufacturer’s ROM. Some models have added a permission – background pop-up interface. For example, this permission is added on MIUI and is closed by default. Unless they are added to their white list, it is explained in the document of Xiaomi open platform: this permission is rejected by default. Both applications are not allowed to pop up pages in the background by default, and a white list will be provided for special applications, such as music (lyrics display), sports, VoIP (incoming calls), etc.; once malicious acts such as promotion occur in the whitelist application, the whitelist will be permanently cancelled.

    Detect the permission of background pop-up interface

    On the Xiaomi model, the newly added permission of the background pop-up interface is to expand the new permission in appopsservice. You can see many familiar constants by viewing the appopsmanager source code:

    
    @SystemService(Context.APP_OPS_SERVICE)
    public class AppOpsManager {
        public static final int OP_GPS = 2;
        public static final int OP_READ_CONTACTS = 4;
        // ...
    }
    

    Therefore, you can check whether you have the permission of the background pop-up interface through appopsservice. What is the opcode corresponding to this permission? An insider on the Internet revealed that the code of this permission is 10021, so you can use appopsmanager.checkopnostrow or appopsmanager.noteopnostrow and other methods to detect whether the permission exists. However, these methods are identified by @ hide and need to use reflection:

    
    fun checkOpNoThrow(context: Context, op: Int): Boolean {
        val ops = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        try {
            val method: Method = ops.javaClass.getMethod(
                "checkOpNoThrow", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java
            )
            val result = method.invoke(ops, op, myUid(), context.packageName) as Int
            return result == AppOpsManager.MODE_ALLOWED
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return false
    }
    
    fun noteOpNoThrow(context: Context, op: Int): Int {
        val ops = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        try {
            val method: Method = ops.javaClass.getMethod(
                "noteOpNoThrow", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, String::class.java
            )
            return method.invoke(ops, op, myUid(), context.packageName) as Int
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return -100
    }
    

    In addition, if you want to know the codes of other newly added permissions, you can traverse the permissions of codes in a certain range (such as 10000 ~ 10100) through the above method, and then operate the mobile phone to switch the permissions you want to query. According to the traversal results, you can roughly get the code of the corresponding permissions.

    Android P background startup permission

    The test on Xiaomi max3 found two ways to start the activity interface from the background. Its system is the MIUI system based on Android 9.

    Method 1: movetasktofront

    This method is not to start the activity directly from the background, but a different idea. Before starting the target activity in the background, first switch the application to the foreground, and then start the target activity. If necessary, you can also move the activity previously switched to the foreground back to the background through the activity.movetasktoback method. After testing, This method has failed on Android 10… But versions below 10 can be salvaged (Android. Permission. Reorder_tasks permission needs to be declared).

    Before starting the target activity, first judge whether the application is in the background. The judgment method can use the activitymanager.getrunningappprocesses method or application.activitylifecyclecallbacks to monitor the front and background. Both methods are explained in articles on the Internet, so I won’t repeat them. Directly post the code for switching from the background to the foreground:

    fun moveToFront(context: Context) {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
        activityManager?.getRunningTasks(100)?.forEach { taskInfo ->
            if (taskInfo.topActivity?.packageName == context.packageName) {
                Log.d("LLL", "Try to move to front")
                activityManager.moveTaskToFront(taskInfo.id, 0)
                return
            }
        }
    }
    
    fun startActivity(activity: Activity, intent: Intent) {
        if (!isRunningForeground(activity)) {
            Log.d("LLL", "Now is in background")
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                //Todo prevents movetofront from failing. You can try to call it several times
                moveToFront(activity)
                activity.startActivity(intent)
                activity.moveTaskToBack(true)
            } else {
                NotificationUtils.sendNotificationFullScreen(activity, "", "")
            }
        } else {
            Log.d("LLL", "Now is in foreground")
            activity.startActivity(intent)
        }
    }

    Method 2: hook

    As the MIUI system is not open source, try to study the AOSP source code again, and see if you can find any clues. First, start with the activity.startactivity method. If you have read the activity startup source code process, you can know that the activity.startactivity or call it to instrumentation.execstartactivity, and then call the relevant methods of AMS through binder. The permission authentication is completed in AMS. If the permission does not meet the requirements, the natural startup fails (Android 10).

    //App process
    public ActivityResult execStartActivity(Context who, IBinder contextThread, ...) {
        // ...
        //Here, AMS related code will be called through binder
        int result = ActivityManager.getService().startActivity(whoThread, who.getBasePackageName(),
                intent, intent.resolveTypeIfNeeded(who.getContentResolver()),
                token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
        // ...
    }
    
    // system_ Server process
    // AMS
    public final int startActivity(IApplicationThread caller, String callingPackage, Intent intent,
        String resolvedType, IBinder resultTo, String resultWho, int requestCode,
        int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {
        // ...
    }

    Take a look at these parameters:

    • Caller: AMS will use it to call the client app process to instantiate the activity object and call back its lifecycle method after completing relevant tasks. The binder server of caller is located in the app process.
    • Callingpackage: this parameter identifies the caller package name.

    Here you can try some system things of hook. I won’t give the specific hook code first. After testing, it can succeed on Xiaomi devices of Android 9. If you are interested, you can study and talk about it yourself. It’s not public for the time being. Students in need can leave a message to tell me. Or decompile Xiaomi ROM source code, you can find something from it.

    Android Q background startup permission

    As mentioned above, starting from the Android Q version, the native system also adds the restriction of background startup. You can start the activity from the background on the native Android 10 system by notifying and setting fullscreenintent. Check the AOSP source code. You can find this part of the code for background permission restriction in AMS. The above describes the process of startactivity. After the app process initiates a request, it will call the system across processes through binder_ The AMS in the server process is then invoked to the ActivityStarter.startActivity method.

    //Good guy, more than 20 parameters, hey, hey, hey
    private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            SafeActivityOptions options,
            boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
            TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup,
            PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) {
        // ...
        boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
            requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity,
            inTask != null, callerApp, resultRecord, resultStack);
        abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
                callingPid, resolvedType, aInfo.applicationInfo);
        abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid,
                callingPackage);
    
        boolean restrictedBgActivity = false;
        if (!abort) {
            restrictedBgActivity = shouldAbortBackgroundActivityStart(callingUid,
                    callingPid, callingPackage, realCallingUid, realCallingPid, callerApp,
                    originatingPendingIntent, allowBackgroundActivityStart, intent);
        }
        // ...
    }

    The shouldabortbackgroundactivitystart call here is newly added in Android Q. you can see the kitchen knife according to the method name. It is started in the background:

    
    boolean shouldAbortBackgroundActivityStart(...) {
        final int callingAppId = UserHandle.getAppId(callingUid);
        if (callingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID
                || callingAppId == Process.NFC_UID) {
            return false;
        }
        if (callingUidHasAnyVisibleWindow || isCallingUidPersistentSystemProcess) {
            return false;
        }
        // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
        if (mService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid)
                == PERMISSION_GRANTED) {
            return false;
        }
        // don't abort if the caller has the same uid as the recents component
        if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
            return false;
        }
        // don't abort if the callingUid is the device owner
        if (mService.isDeviceOwner(callingUid)) {
            return false;
        }
        // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
        if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
            Slog.w(TAG, "Background activity start for " + callingPackage
                    + " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
            return false;
        }
        // ...
    }
    

    From this method, we can see that the restrictions on background startup can correspond to the instructions in the official document on the restrictions on background startup of activity, which are used to judge the permission of uid, and are in the system process system_ Server. Simply changing the package name is useless…

    On some ROMs that do not have separate restrictions on background startup, the background activity page can be successfully popped up through full screen notification, such as Xiaomi A3, another vivo and a Samsung mobile phone. The specific model has been forgotten; It doesn’t pop up on restricted devices, such as red rice note 8 pro.

    For the hard bone of red rice note 8 pro, I kept trying many methods, but in fact, I took a chance because I couldn’t get the source code of MIUI. Later, I wanted to change my mind. Can I try to pull out the relevant framework.jar package from this mobile phone and decompile it? Maybe there will be a harvest! However, it needs a root phone. It’s easy to do. Xiaomi has a development version system that can provide root, so I went to the MIUI official website and found that this Hongmi note 8 Pro model does not provide a development version system (laughing and crying). It seems that Xiaomi said that Xiaomi will no longer provide a development version for the low-end machine before… Well, there’s no other phone to try.

    On second thought, can I download the stable version of the ROM package directly? After decompression, is there any tool that can get some traces related to the source code? So I downloaded a rom.zip and unzipped it. I saw that there were only some system image img files and. Dat.br files in it. I don’t quite understand this. I guess even if I can get what I want, the time and cost of the whole process is estimated to exceed expectations, so I can only put down this idea for the time being. There will be enough time for further research.

    summary

    Native Android ROM

    Android native ROM can normally launch the activity interface from the background, whether Android 9 (direct launch) or 10 (with full screen notification).

    Customized ROM

    Detect the permission of the pop-up interface in the background:

    • Detect the permission of the corresponding opcode by reflecting appopsmanager related methods;
    • Opcode = 10021 (millet model);
    • Other models can try to traverse to get opcode;

    Android p version of Xiaomi:

    • Start the activity in the background through hook related parameters. The code cannot be given for some reasons. Students in need can leave a message and tell me ha;
    • Only Xiaomi models have been tested, and other models may not be available;
    • Theoretically, millet below p version should also be supported;

    Android p version models:

    • Switch the application to the foreground through the movetasktofront method;
    • This method is the official API after all, so the compatibility may be better;
    • If the switch fails, you can try calling the movetasktofront method several more times;
    • Theoretically, models below p version should also be supported;

    Android Q version models:

    • Activate the background activity through full screen notification;
    • It may fail to start on some ROM with other restrictions;

    As for the way to decompile MIUI code, it is only a guess, and the time reason has not been put into action. It seems that the product brother’s needs can not be fully realized for the time being. I don’t know if there are any small partners who have done relevant research (or know the inside story) can provide some reference ideas. Although it is a rogue function, the code is innocent. Hey hey, hey, move towards a demand goal, think about solutions and investigate from all directions, I think it is an interesting and uplifting thing! Students with relevant research are welcome to put forward suggestions in the comment area and do a good job in the needs.

    The above is the details of the implementation example of Android background startup activity. For more information about Android background startup activity, please pay attention to other relevant articles of developeppaer!

    Recommended Today

    SQL statement of three-level linkage of provinces, cities and counties

    The first is the table creation statement Copy codeThe code is as follows: CREATE TABLE `t_address_province` ( `id` INT AUTO_ Increment primary key comment ‘primary key’,`Code ` char (6) not null comment ‘province code’,`Name ` varchar (40) not null comment ‘province name’)Engine = InnoDB default charset = utf8 comment = ‘province information table’; CREATE TABLE […]