实战_Android后台启动Activity实践之路,android工程师面试题目和答案

}

fun noteOpNoThrow(context: Context, op: Int): Int {
val ops = context.getSystemService(Context.APP_OPS_SERVICE) as AppOp实战_Android后台启动Activity实践之路,android工程师面试题目和答案
sManager
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
}

另外如果想知道其它新增权限的 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? 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 防止 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进程
// 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) {
// …
}

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

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

这里可以尝试 Hook 一些系统的东西,具体怎么 Hook 的代码先不给出了,经过测试在 Android 9 的小米设备上可以成功,有兴趣可以自行研究谈论哈,暂时不公开了,有需要的同学可以留言告诉我。或者反编译小米 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);
}
// …
}

这里的 shouldAbortBackgroundActivityStart 调用是在 Android Q 中新增的,看方法名就能菜刀这是针对后台启动的:

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;
    }
    // …
    }

从这个方法可以看到后台启动的限制和官方文档 从后台启动 Activity 的限制 中的说明是可以对应上的,这里面都是针对 uid 去做权限判断的,且是在系统进程 system_server 中完成,单纯更改包名已经没用了。。。

在一些没有针对后台启动单独做限制的 ROM 上通过 全屏通知 可以成功弹出后台 Activity 页面,比如说小米 A3,另外还有一台 vivo 和一台三星手机,具体机型忘记了;在做了限制的设备上则弹不出来,比如说红米 Note 8 Pro。

对于红米 Note 8 Pro 这块硬骨头,不停尝试了好多方法,但其实都是碰运气的,因为拿不到 MIUI 的源码,后来想转变思路,是否可以尝试从这台手机上 pull 出相关的 framework.jar 包然后反编译呢?说不定就有收获!不过需要 Root 手机,这个好办,小米自己是有提供可以 Root 的开发版系统的,于是就去 MIUI 官网找了一下,发现这台红米 Note 8 Pro 机型没有提供开发版系统(笑哭),想起来好像之前是说过低端机小米不再提供开发版了。。。好吧,手里头没有其它可以尝试的手机了。

再转念一想,是否可以直接下载稳定版的 ROM 包,解压后有没有工具能够得到一些源码相关的痕迹呢?于是下载了一个 ROM.zip 后,解压看到里面只有一些系统映像 img 文件和 .dat.br 文件,这一块我还不太懂,猜想就算能得到我想要的东西,整套流程花费的时间成本估计也超出预期了,所以暂时只能先放下这个想法了。后续有足够的时间再深入研究研究吧。

总结

原生Android ROM

Android 原生 ROM 都能正常地从后台启动 Activity 界面,无论是 Android 9(直接启动) 还是 10 版本(借助全屏通知)。

定制化ROM

检测后台弹出界面权限:

也超出预期了,所以暂时只能先放下这个想法了。后续有足够的时间再深入研究研究吧。

总结

原生Android ROM

Android 原生 ROM 都能正常地从后台启动 Activity 界面,无论是 Android 9(直接启动) 还是 10 版本(借助全屏通知)。

定制化ROM

检测后台弹出界面权限:

上一篇:08 生产问题-为什么隔一段时间就来一次堆内存溢出


下一篇:广播接收者BroadcastReceiver