写点什么

实战 _Android 后台启动 Activity 实践之路

用户头像
Android架构
关注
发布于: 17 小时前

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()}}


到现在,整体上感觉还是不错的,现阶段的 Android 原生 ROM 都能正常地从后台启动 Activity 界面,无论是 Android 9 还是 10 版本,都美滋滋。

定制化 ROM

问题开始浮出水面,由于各大厂商对 Android 的定制化各有不一,而 Android 并没有继承 GPL 协议,它使用的是 Apache 开源许可协议,即第三方厂商在修改代码后可以闭源,因此也无法得知厂商 ROM 的源码到底做了哪些修改。有的机型增加了一项权限——后台弹出界面,比如说在 MIUI 上便新增了这项权限且默认是关闭的,除非加入了它们的白名单,小米开放平台的文档 里有说明:该权限默认为拒绝的,既为应用默认不允许在后台弹出页面,针对特殊应用会提供白名单,例如音乐(歌词显示)、运动、VOIP(来电)等;白名单应用一旦出现推广等恶意行为,将永久取消白名单。

检测后台弹出界面权限

在小米机型上,新增的这个 后台弹出界面 的权限是在 AppOpsService 里扩展了新的权限,查看 AppOpsManager 源代码,可以在里面看到许多熟悉的常量:


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


因此可以通过 AppOpsService 来检测是否具有 后台弹出界面 的权限,那么这个权限对应的 OpCode 是啥呢?网上有知情人士透露这个权限的 Code 是 10021,因此可以使用 AppOpsManager.checkOpNoThrow 或 AppOpsManager.noteOpNoThrow 等系列的方法检测该权限是否存在,不过这些方法都是 @hide 标识的,需要使用反射:


fun checkOpNoThrow(context: Context, op: Int): Boolean {val ops = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManagertry {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 Intreturn 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 AppOpsManagertry {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}


另外如果想知道其它新增权限的 code, 可以通过上面的方法去遍历某个范围(如 10000~10100)内的 code 的权限,然后手机操作去开关想要查询的权限,根据遍历的结果,就大致可以得到对应权限的 code 了。

Android P 后台启动权限

在小米 Max3 上测试发现了两种方式可以实现从后台启动 Activity 界面,其系统是基于 Android 9 的 MIUI 系统。


方式一:moveTaskToFront


这种方式不算是直接从后台启动 Activity,而是换了一个思路,在后台启动目标 Activity 之前先将应用切换到前台,然后再启动目标 Activity,如果有必要的话,还可以通过 Activity.moveTaskToBack 方法将之前切换到前台的 Activity 重新移入后台,经过测试,在 Android 10 上这个方法已经失效了...但是 10 以下的版本还是可以抢救一下的(需要声明 android.permission.REORDER_TASKS 权限)。


启动目标 Activity 之前先判断一下应用是否在后台,判断方法可以借助 ActivityManager.getRunningAppProcesses 方法或者 Application.ActivityLifecycleCallbacks 来监听前后台,这两种方法网上都有文章讲解,就不赘述了。直接贴出后台切换到前台的代码:


fun moveToFront(context: Context) {val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManageractivityManager?.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 防止 moveToFront 失败,可以多尝试调用几次 moveToFront(activity)activity.startActivity(intent)activity.moveTaskToBack(true)} else {NotificationUtils.sendNotificationFullScreen(activity, "", "")}} else {Log.d("LLL", "Now is in foreground")activity.startActivity(intent)}}


方式二:Hook


由于 MIUI 系统不开源,因此尝试再研究研究 AOSP 源码,死马当活马医看能不能找到什么蛛丝马迹。首先从 Activity.startActivity 方法开始追,如果阅读过 Activity 启动源码流程的话可以知道 Activity.startActivity 或调用到 Instrumentation.execStartActivity 中,然后通过 Binder 调用到 AMS 相关的方法,权限认证就在 AMS 中完成,如果权限不满足自然启动就失败了(Android 10)。


// APP 进程 public ActivityResult execStartActivity(Context who, IBinder contextThread, ...) {// ...// 这里会通过 Binder 调用到 AMS 相关的代码 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 进程// AMSpublic final int startActivity(IApplicationThread caller, String callingPackage, Intent intent,String resolvedType, IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {// ...}


这里如果对 system_server, zygote 进程这些感兴趣的话可以参考 Android系统启动源码解读-init-zygote-system_server。看一下这几个参数:


  • caller: AMS 在完成相关任务后会通过它来 Binder 调用到客户端 APP 进程来实例化 Activity 对象并回调其生命周期方法,caller 的 Binder 服务端位于 APP 进程。

  • callingPackage: 这个参数标识调用者包名。

  • ...


这里可以尝试 Hook 一些系统的东西,具体怎么 Hook 的代码先不给出了,经过测试在 Android 9 的小米设备上可以成功,有兴趣可以自行研究谈论哈,暂时不公开了,有需要的同学


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


可以留言告诉我。或者反编译小米 ROM 源码,可以从里面发现一些东西。

Android Q 后台启动权限

在上面介绍过 Android Q 版本开始原生系统也加入了后台启动的限制,通过通知设置 fullScreenIntent 可以在原生 Android 10 系统上从后台启动 Activity。查看 AOSP 源码,可以在 AMS 找到这部分后台权限限制的代码,上面讲到 startActivity 的流程,在 APP 进程发起请求后,会通过 Binder 跨进程调用到 system_server 进程中的 AMS,然后调用到 ActivityStarter.startActivity 方法,关于后台启动的限制就这这里:


// 好家伙,整整二十多个参数,嘿嘿,嘿嘿 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);}// ...}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
实战_Android后台启动Activity实践之路