本文对照的源码是8.0.0_r1
通常我们会这样使用Toast
Toast.makeText(context, message, duration).show()
而当我们在手机设置中取消你的应用通知管理时,就会发现Toast消失了。下面我们从源码的角度分析Toast的调用与被系统屏蔽。
首先会调用Toast中的show()方法
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
//1
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
//2
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
在注释1处我们会通过getService()获得INotificationManager,在getService()中通过AIDL跨进程获取通知服务管理。并在注释2中调用enqueueToast方法。由系统启动源码我们知道NotificationManagerService是在SystemServer进程中启动,下面看一下NotificationManagerService。在源码的frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService
private final IBinder mService = new INotificationManager.Stub() {
// Toasts
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
//1是否是系统弹窗
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
//2 弹窗依据
final boolean isPackageSuspended =
isPackageSuspendedForUser(pkg, Binder.getCallingUid());
//3在这里判断是否允许你的应用弹窗
if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
(!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
|| isPackageSuspended)) {
Slog.e(TAG, "Suppressing toast from package " + pkg
+ (isPackageSuspended
? " due to package suspended by administrator."
: " by user request."));
return;
}
//省略不相关代码...
}
}
在NotificationManagerService源码中我们可以看到在注释1处判断了是否为系统级弹窗,判断进程id是否是系统进程,或者包名是否是'android',这里为我们Toast弹窗伪装成系统弹窗提供了方法,在后面我们会说到。在注释2处调用了isPackageSuspendedForUser(pkg, Binder.getCallingUid()),在注释3处判断是否可以弹窗。先看一下注释2处的方法最终会调用frameworks/base/services/core/java/com/android/server/pm/PackageManagerService
@Override
public boolean isPackageSuspendedForUser(String packageName, int userId) {
final int callingUid = Binder.getCallingUid();
//1 检查用户权限
enforceCrossUserPermission(callingUid, userId,
true /* requireFullPermission */, false /* checkShell */,
"isPackageSuspendedForUser for user " + userId);
synchronized (mPackages) {
final PackageSetting ps = mSettings.mPackages.get(packageName);
if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
throw new IllegalArgumentException("Unknown target package: " + packageName);
}
//2根据线程id返回是否暂停
return ps.getSuspended(userId);
}
}
在注释1处isPackageSuspendedForUser方法会调用PackageManager.isPackageSuspendedForUser检查用户交互权限“android.Manifest.permission.INTERACT_ACROSS_USERS_FULL” 。
private boolean isPackageSuspendedForUser(String pkg, int uid) {
int userId = UserHandle.getUserId(uid);
try {
return mPackageManager.isPackageSuspendedForUser(pkg, userId);
} catch (RemoteException re) {
throw new SecurityException("Could not talk to package manager service");
} catch (IllegalArgumentException ex) {
// Package not found.
return false;
}
}
在注释2处在最后会调用PackageSetting的getSuspended获取当前用户进程状态是否是暂停。暂停时也是不能发送Toast的。
最后分析不能发送Toast最后一个依据areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())这个方法会判断通知是否开启,未开启不能Toast。
上面介绍了Toast能否弹出的情况,通过源码分析在通知关闭时是无法Toast的,要想应用在关闭通知的情况下Toast我们需要伪装成系统应用。这里我们用hook进行替换Toast.enqueueToast方法的参数伪装成系统Toast。
try {
//1通过反射获取Toast的getService方法
Method serviceMethod = Toast.class.getDeclaredMethod("getService");
serviceMethod.setAccessible(true);
//2调用 toast 中的getService() 方法 返回INotificationManager类型的Object
Object iNotificationManagerObj = serviceMethod.invoke(toast);
//3反射获取INotificationManager的Class
Class iNotificationManagerCls = Class.forName("android.app.INotificationManager");
//4创建 INotificationManager的代理对象 替换Toast中的 INotificationManager
Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerCls},
(proxy, method, args) -> {
//强制使用系统Toast
if ("enqueueToast".equals(method.getName())
|| "enqueueToastEx".equals(method.getName())) { //华为p20 pro上为enqueueToastEx
//5上文中pkg 为“android”时为系统弹窗
args[0] = "android";
}
Log.e("test", "强制使用系统Toast>>>>>>>>>");
return method.invoke(iNotificationManagerObj, args);
});
//6进行替换
Field sServiceFiled = Toast.class.getDeclaredField("sService");
sServiceFiled.setAccessible(true);
sServiceFiled.set(toast, iNotificationManagerProxy);
}
toast.show();
} catch (Exception e) {
e.printStackTrace();
}
到此我们从源码分析了Toast失效到,用hook解决Toast的失效,源码会在封装后上传至github。感谢大佬 刘望舒的《Android进阶解密》在framework层阅读源码及了解整个Android系统的启发,不在惧怕底层源码,要做到知其然知其所以然。
网友评论