部分机型在用户通知权限关闭之后,toast弹不出来,导致app变成哑巴应用,在需要toast提示用户的地方,没有任何展示。
使用三星S8(android9.0)在关闭通知权限之后可以复现该场景。
我们不能要求用户开启通知权限,那么需要兼容用户关闭通知权限的场景。
具体分析
我们看一下toast的show()方法做了什么:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called"); }
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty }
}
调用了INotificationManager的enqueueToast方法,可以看出toast使用了notification的相关api,需要通知权限。
对于一些定制的系统,应该是实现了自己的aidl代理类,重写了enqueueToast方法,可以做到不受通知栏权限的限制。比如三星s8的该方法名称就是enqueueToastLog。
看下系统的INotificationManager代理类NotificationManagerService:
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) {
Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
+ " duration=" + duration);
}
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
...}
通过isSystemToast这个变量判断是不是系统的toast,如果是系统toast那么就不受通知权限的影响了。
尝试hook enqueueToast方法,修改packgae名称为android,让isSystemToast这个参数为true。
Method getServiceMethd = Toast.class.getDeclaredMethod("getService");
getServiceMethd.setAccessible(true);
final Object iNotificationManager = getServiceMethd.invoke(null);
Class iNmCls = Class.forName("android.app.INotificationManager");
Object proxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNmCls}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("enqueueToast".equals(method.getName()) || "enqueueToastEx".equals(method.getName()) || "enqueueToastLog".equals(method.getName())){
args[0] = "android";
}
return method.invoke(iNotificationManager,args);
}
});
Field sServiceField = Toast.class.getDeclaredField("sService");
sServiceField.setAccessible(true);
sServiceField.set(null,proxy);
但是Android10上报错,如下:
看来Android10.0上对uid也做了限制,不能做到兼容全部版本。
我们知道Toast是使用WindowManager创建的,那么我们也可以手动创建一个toast,展示在当前的activity中,通过定时任务关闭它,实现toast的效果。
优点是在用户通知权限关闭之后也能展示toast。
因windowmanager是跟当前activity绑定,那么跟系统的toast比起来,在当前activity关闭之后就不再继续展示了。
实现方式
在用户通知权限开启的时候,使用原生toast,未开启时使用windowmanager添加toast。
在baseApplication中调用init:
public static void init(Application application){
if (mToast != null) {
mToast.cancel();
}
if(DuToastCompat.isNotificationEnabled(application)){
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
mToast = new SafeToast(application);
}else{
mToast = new DuToast(application);
}
}else{
mToast = new SupportNotificationToast(application);
}
if (sStrategy == null) {
setToastHandler(new ToastStrategy());
}
}
对应的类 SafeToast:兼容7.1版本的主线程阻塞问题
NotificationSupportToast:通知权限未开启时候使用
DuToast:封装原生toast
ToastStrategy:实际是个handler,结合队列控制toast的show和cancel时机。
可实现最多三个toast依次展示,不会因后续toast的弹出导致之前toast显示时间过短。
展示在底部的toast沿用之前的调用方式ToastUtil.show(),其他样式可直接调用DuToastUtils中的方法。
showToastInCenter //显示在中间位置的toast
showToastWithIconInCenter //显示在中间位置的toast,带图片,一行文字
showToastIconWithTwoTextInCenter //显示在中间位置的toast,一张图片,两行文字



