Skip to content

Commit fd0dae1

Browse files
committed
修复 Toast 在关闭通知栏权限后无法在后台显示的问题
新增支持 ActivityToast 设置显示动画和显示时长
1 parent ab153ba commit fd0dae1

File tree

18 files changed

+356
-149
lines changed

18 files changed

+356
-149
lines changed

README.md

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
* 博客地址:[只需体验三分钟,你就会跟我一样,爱上这款 Toast](https://www.jianshu.com/p/9b174ee2c571)
66

7-
* 已投入公司项目多时,没有任何毛病,可胜任任何需求,[点击此处下载Demo](ToastUtils.apk)
7+
* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/ToastUtils/releases/download/10.0/ToastUtils.apk)
88

9-
![](ToastUtils.jpg)
9+
![](picture/demo_code.png)
10+
11+
![](picture/demo_page.jpg)
1012

1113
#### 集成步骤
1214

@@ -38,7 +40,7 @@ android {
3840
3941
dependencies {
4042
// 吐司框架:https://github.com/getActivity/ToastUtils
41-
implementation 'com.github.getActivity:ToastUtils:9.6'
43+
implementation 'com.github.getActivity:ToastUtils:10.0'
4244
}
4345
```
4446

@@ -97,41 +99,67 @@ ToastUtils.setInterceptor(IToastInterceptor interceptor);
9799
ToastUtils.getInterceptor();
98100
```
99101

102+
* 如果你需要对 Toast 的进行定制化,可以使用以下方式
103+
104+
```java
105+
ToastUtils.init(this, new ToastStrategy() {
106+
107+
@Override
108+
public IToast createToast(Application application) {
109+
IToast toast = super.createToast(application);
110+
if (toast instanceof ActivityToast) {
111+
ActivityToast activityToast = ((ActivityToast) toast);
112+
// 设置 Toast 动画效果
113+
activityToast.setAnimationsId(R.anim.xxx);
114+
// 设置短 Toast 的显示时长(默认是 2000 毫秒)
115+
activityToast.setShortDuration(1000);
116+
// 设置长 Toast 的显示时长(默认是 3500 毫秒)
117+
activityToast.setLongDuration(5000);
118+
}
119+
return toast;
120+
}
121+
});
122+
```
123+
124+
* 这种方式的缺点是只有应用在前台的情况下才会生效,这是因为前台的 Toast 是用框架实现的,本质上是一个 WindowManager,优点是非常灵活,不受原生 Toast 机制限制,缺点是无法在后台的情况下显示;而后台的 Toast 是用系统来实现的,优点是能在后台的情况下显示,缺点是局限性非常大,无法做太深的定制化;而框架正是利用了两种方式的优缺点进行了互补。
125+
100126
### 温馨提示:框架意在解决一些常规的 Toast 需求,如果是有一些特殊的定制化需求请配搭 [XToast](https://github.com/getActivity/XToast) 悬浮窗框架使用
101127

102-
### 不同 Toast 框架之间的对比
128+
#### 不同 Toast 框架之间的对比
103129

104130
| 功能或细节 | [ToastUtils](https://github.com/getActivity/ToastUtils) | [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) | [Toasty](https://github.com/GrenderG/Toasty) |
105131
| :----: | :------: | :-----: | :-----: |
106-
| 对应版本 | 9.6 | 1.30.6 | 1.5.0 |
132+
| 对应版本 | 10.0 | 1.30.6 | 1.5.0 |
107133
| issues 数 | [![](https://img.shields.io/github/issues/getActivity/ToastUtils.svg)](https://github.com/getActivity/ToastUtils/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/GrenderG/Toasty.svg)](https://github.com/GrenderG/Toasty/issues) |
108-
| **aar 包大小** | 22 KB | 500 KB | 50 KB |
134+
| **aar 包大小** | 26 KB | 500 KB | 50 KB |
109135
| **调用代码定位** ||||
110136
| 支持在子线程中调用显示 ||||
111137
| 支持全局设置统一 Toast 样式 ||||
112138
| 处理 Toast 在 Android 7.1 崩溃的问题 ||||
113-
| 兼容通知栏权限关闭后 Toast 显示不出来的问题 || ||
114-
| 适配 Android 11 不能在后台显示自定义样式 Toast 的问题 ||||
139+
| 兼容通知栏权限关闭后 Toast 显示不出来的问题 || ||
140+
| 适配 Android 11 不能在后台显示 Toast 的问题 ||||
115141

116142
#### 调用代码定位功能介绍
117143

118-
* 框架会在日志打印中输出在 Toast 调用的代码位置,这样开发者可以直接通过点击 Log 来定位是在哪个类哪行代码调用的,这样可以极大提升我们排查问题的效率,特别是 Toast 的内容是由后台返回的情况下,我相信没有一个人会拒绝这样的功能
144+
* 框架会在日志打印中输出在 Toast 调用的代码位置,这样开发者可以直接通过点击 Log 来定位是在哪个类哪行代码调用的,这样可以极大提升我们排查问题的效率,特别是 Toast 的内容是由后台返回的情况下,我相信没有任何一个人会拒绝这样的功能
119145

120-
![](RequestCode.jpg)
146+
![](picture/logcat_code.jpg)
121147

122148
#### Toast 在 Android 7.1 崩溃的问题介绍
123149

124-
* 这个问题是由于 Android 7.1 加入 WindowToken 校验机制导致的,而这个 WindowToken 是 NotificationManagerService 生成的,这个 WindowToken 是存在一定时效性的,而当应用的主线程被阻塞时,WindowManager 在 addView 时会对 WindowToken 进行校验,但是 WindowToken 已经过期了,这个时候 addView 的时候就会抛出异常
150+
* 这个问题是由于 Android 7.1 加入 WindowToken 校验机制导致的,而这个 WindowToken 是 NotificationManagerService 生成的,这个 WindowToken 是存在一定时效性的,而当应用的主线程被阻塞时,WindowManager 在 addView 时会对 WindowToken 进行校验,但是 WindowToken 已经过期了,这个时候 addView 就会抛出异常
125151

126152
* 谷歌在 Android 8.0 就修复了这个问题,修复方式十分简单粗暴,就是直接捕获这个异常,而框架的修复思路跟谷歌类似,只不过修复方式不太一样,因为框架无法直接修改系统源码,所以是直接通过 Hook 的方式对异常进行捕获,大家如果对修复过程感兴趣可以看一下我写的这篇文章[Toast 在 Android 7.1 崩溃排查及修复](https://www.jianshu.com/p/437f473017d6)
127153

128154
#### 通知栏权限关闭后 Toast 显示不出来的问题介绍
129155

130-
* 这个问题的出现是因为原生 Toast 的显示要通过 NMS(NotificationManagerService) 才会 addView 到 Window 上面,而在 NMS 中有一个 `static final boolean ENABLE_BLOCKED_TOASTS = true` 的字段,当这个常量值为 true 时,会触发 NMS 对应用通知栏权限的检查,如果没有通知栏权限,那么这个 Toast 将会被 NMS 所拦截,并输出 `Suppressing toast from package` 日志信息,而小米手机没有这个问题是因为它是将 `ENABLE_BLOCKED_TOASTS` 字段值修改成 `false`,所以就不会触发对通知栏权限的检查,另外我为什么会知道有这个事情?因为我曾经和一名 MIUI 工程师一起确认过这个事情,不过值得一提的是,这个问题在 Android 9.0 的版本已经修复了。
156+
* [Toast通知栏权限填坑指南](https://www.jianshu.com/p/1d64a5ccbc7c)
157+
158+
* 这个问题的出现是因为原生 Toast 的显示要通过 NMS(NotificationManagerService) 才会 addView 到 Window 上面,而在 NMS 中有一个 `static final boolean ENABLE_BLOCKED_TOASTS = true` 的字段,当这个常量值为 true 时,会触发 NMS 对应用通知栏权限的检查,如果没有通知栏权限,那么这个 Toast 将会被 NMS 所拦截,并输出 `Suppressing toast from package` 日志信息,而小米手机没有这个问题是因为它是将 `ENABLE_BLOCKED_TOASTS` 字段值修改成 `false`,所以就不会触发对通知栏权限的检查,另外我为什么会知道有这个事情?因为我曾经和一名 MIUI 工程师一起确认过这个事情。
131159

132-
* 框架处理这个问题的方式很简单,是在显示 Toast 的时候判断是否有通知栏权限,如果有则显示系统的 Toast,如果没有则使用 WindowManager 来代替显示,大家如果对修复过程感兴趣可以看一下我写的这篇文章[Toast通知栏权限填坑指南](https://www.jianshu.com/p/1d64a5ccbc7c)
160+
* 框架处理这个问题的方式有两种,先判断当前应用是否处于前台状态,如果是则使用自定义的 WindowManager 代替 Toast 来显示,如果当前应用处于后台状态,则会通过 Hook Toast 中的 INotificationManager 接口,将 enqueueToast 方法传递的包名参数修改成 `android` 来欺骗 NotificationManagerService,因为 NotificationManagerService 已经将 `android` 包名的应用纳入白名单,会自动放行,需要注意的是,这种方式在 Android 10 上面已经失效了,已经被系统纳入反射黑名单,但是好消息是,通过查看和对比 NotificationManagerService 源码发现,这个问题已经在 Android 10.0 的版本上面被修复了,所以框架只在 Android 9.0 及以下版本并且在关闭了通知栏权限的情况下才去 Hook INotificationManager,这样就能比较完美地解决这一问题,另外我还找到了官方关于这块的代码提交记录:[Always allow toasts from foreground apps](https://cs.android.com/android/_/android/platform/frameworks/base/+/58b2453ed69197d765c7254241d9966ee49a3efb),大家可以感兴趣可以看看
133161

134-
#### Android 11 不能在后台显示自定义样式 Toast 的问题介绍
162+
#### Android 11 不能在后台显示 Toast 的问题介绍
135163

136164
* 当我们将 targetSdkVersion 改成 30 及以上的版本时,会发现一个问题,如果应用处于后台进程的情况下,而恰好我们的应用 Toast 样式是经过定制的,那么在这些情况下调用 Toast 的 show 方法会惊奇的发现,Toast 没有显示出来,请注意这个问题不是 Bug,而是 Android 11 禁止了这种行为,在 [Toast 官方文档](https://developer.android.google.cn/reference/android/widget/Toast#setView(android.view.View)) 中也有注明,不建议对 Toast 的样式做定制化,并且还对 `Toast.setView` 方法进行了标记过时处理。
137165

@@ -165,8 +193,6 @@ ToastUtils.getInterceptor();
165193
Toast\.makeText\([^,]+,\s*(.+{1}),\s*[^,]+\)\.show\(\)
166194
```
167195

168-
---
169-
170196
```text
171197
ToastUtils.show($1)
172198
```
@@ -177,8 +203,6 @@ ToastUtils.show($1)
177203
import android.widget.Toast
178204
```
179205

180-
---
181-
182206
```text
183207
import com.hjq.toast.ToastUtils
184208
```
@@ -238,4 +262,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
238262
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
239263
See the License for the specific language governing permissions and
240264
limitations under the License.
241-
```
265+
```

ToastUtils.apk

-4.61 MB
Binary file not shown.

app/build.gradle

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
apply plugin: 'com.android.application'
22

33
android {
4-
compileSdkVersion 30
4+
compileSdkVersion 31
55

66
defaultConfig {
77
applicationId "com.hjq.toast.demo"
88
minSdkVersion 16
9-
targetSdkVersion 30
10-
versionCode 96
11-
versionName "9.6"
9+
targetSdkVersion 31
10+
versionCode 1000
11+
versionName "10.0"
1212
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
1313
}
1414

@@ -59,9 +59,6 @@ dependencies {
5959
// Material 库:https://github.com/material-components/material-components-android
6060
implementation 'com.google.android.material:material:1.4.0'
6161

62-
// 权限请求框架:https://github.com/getActivity/XXPermissions
63-
implementation 'com.github.getActivity:XXPermissions:12.3'
64-
6562
// 标题栏框架:https://github.com/getActivity/TitleBar
6663
implementation 'com.github.getActivity:TitleBar:9.2'
6764

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313

1414
<activity
1515
android:name=".ToastActivity"
16-
android:configChanges="orientation|screenSize|keyboardHidden"
17-
android:launchMode="singleTop"
18-
android:screenOrientation="portrait">
16+
android:exported="true">
17+
1918
<intent-filter>
2019
<action android:name="android.intent.action.MAIN" />
2120
<action android:name="android.intent.action.VIEW" />

app/src/main/java/com/hjq/toast/demo/ToastActivity.java

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import androidx.appcompat.app.AppCompatActivity;
1111

1212
import com.google.android.material.snackbar.Snackbar;
13-
import com.hjq.permissions.Permission;
14-
import com.hjq.permissions.XXPermissions;
1513
import com.hjq.toast.ToastUtils;
1614
import com.hjq.toast.style.BlackToastStyle;
1715
import com.hjq.toast.style.WhiteToastStyle;
@@ -63,45 +61,33 @@ public void show5(View v) {
6361
}
6462

6563
public void show6(View v) {
66-
if (XXPermissions.isGranted(this, Permission.NOTIFICATION_SERVICE)) {
64+
Snackbar.make(getWindow().getDecorView(), "正在准备跳转到手机桌面,请系好安全带", Snackbar.LENGTH_SHORT).show();
6765

68-
Snackbar.make(getWindow().getDecorView(), "正在准备跳转到手机桌面,请注意有极少数机型无法在后台显示 Toast", Snackbar.LENGTH_SHORT).show();
69-
70-
v.postDelayed(new Runnable() {
71-
@Override
72-
public void run() {
73-
Intent intent = new Intent(Intent.ACTION_MAIN);
74-
intent.addCategory(Intent.CATEGORY_HOME);
75-
startActivity(intent);
76-
}
77-
}, 2000);
78-
79-
v.postDelayed(new Runnable() {
80-
@Override
81-
public void run() {
82-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
83-
ToastUtils.show("我是在后台显示的 Toast(Android 11 及以上在后台显示 Toast 只能使用系统样式)");
84-
} else {
85-
ToastUtils.show("我是在后台显示的 Toast");
86-
}
87-
}
88-
}, 3000);
89-
90-
} else {
66+
v.postDelayed(new Runnable() {
67+
@Override
68+
public void run() {
69+
Intent intent = new Intent(Intent.ACTION_MAIN);
70+
intent.addCategory(Intent.CATEGORY_HOME);
71+
startActivity(intent);
72+
}
73+
}, 2000);
9174

92-
ToastUtils.show("在后台显示 Toast 需要先获取通知栏权限");
93-
v.postDelayed(new Runnable() {
94-
@Override
95-
public void run() {
96-
XXPermissions.startPermissionActivity(ToastActivity.this, Permission.NOTIFICATION_SERVICE);
75+
v.postDelayed(new Runnable() {
76+
@Override
77+
public void run() {
78+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
79+
ToastUtils.show("我是在后台显示的 Toast(Android 11 及以上在后台显示 Toast 只能使用系统样式)");
80+
} else {
81+
ToastUtils.show("我是在后台显示的 Toast");
9782
}
98-
}, 2000);
99-
}
83+
}
84+
}, 3000);
10085
}
10186

10287
public void show7(View v) {
103-
new XToast<>(ToastActivity.this)
88+
new XToast<>(this)
10489
.setDuration(1000)
90+
// 将 ToastUtils 中的 View 转移给 XToast 来显示
10591
.setContentView(ToastUtils.getStyle().createView(getApplication()))
10692
.setAnimStyle(android.R.style.Animation_Translucent)
10793
.setText(android.R.id.message, "就问你溜不溜")

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
buildscript {
44
repositories {
55
// 阿里云云效仓库:https://maven.aliyun.com/mvn/guide
6-
maven { url 'https://maven.aliyun.com/repository/jcenter' }
6+
maven { url 'https://maven.aliyun.com/repository/public' }
77
maven { url 'https://maven.aliyun.com/repository/google' }
88
// 华为开源镜像:https://mirrors.huaweicloud.com/
99
maven { url 'https://repo.huaweicloud.com/repository/maven/' }
@@ -21,7 +21,7 @@ buildscript {
2121

2222
allprojects {
2323
repositories {
24-
maven { url 'https://maven.aliyun.com/repository/jcenter' }
24+
maven { url 'https://maven.aliyun.com/repository/public' }
2525
maven { url 'https://maven.aliyun.com/repository/google' }
2626
maven { url 'https://repo.huaweicloud.com/repository/maven/' }
2727
maven { url 'https://jitpack.io' }

library/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
apply plugin: 'com.android.library'
22

33
android {
4-
compileSdkVersion 30
4+
compileSdkVersion 31
55

66
defaultConfig {
77
minSdkVersion 14
8-
versionCode 96
9-
versionName "9.6"
8+
versionCode 1000
9+
versionName "10.0"
1010
}
1111

1212
// 使用 JDK 1.8

library/src/main/java/com/hjq/toast/ActivityToast.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* time : 2018/11/02
1313
* desc : 自定义 Toast(用于解决关闭通知栏权限之后不能弹吐司的问题和 Android 11 不能自定义吐司样式的问题)
1414
*/
15-
public final class ActivityToast implements IToast {
15+
public class ActivityToast implements IToast {
1616

1717
/** Toast 实现类 */
1818
private final ToastImpl mToastImpl;
@@ -33,6 +33,12 @@ public final class ActivityToast implements IToast {
3333
private float mHorizontalMargin;
3434
/** 垂直间距 */
3535
private float mVerticalMargin;
36+
/** Toast 动画 */
37+
private int mAnimations = android.R.style.Animation_Toast;
38+
/** 短吐司显示的时长 */
39+
private int mShortDuration = 2000;
40+
/** 长吐司显示的时长 */
41+
private int mLongDuration = 3500;
3642

3743
public ActivityToast(Activity activity) {
3844
mToastImpl = new ToastImpl(activity, this);
@@ -128,4 +134,28 @@ public float getHorizontalMargin() {
128134
public float getVerticalMargin() {
129135
return mVerticalMargin;
130136
}
137+
138+
public void setAnimationsId(int animationsId) {
139+
mAnimations = animationsId;
140+
}
141+
142+
public int getAnimationsId() {
143+
return mAnimations;
144+
}
145+
146+
public void setShortDuration(int duration) {
147+
mShortDuration = duration;
148+
}
149+
150+
public int getShortDuration() {
151+
return mShortDuration;
152+
}
153+
154+
public void setLongDuration(int duration) {
155+
mLongDuration = duration;
156+
}
157+
158+
public int getLongDuration() {
159+
return mLongDuration;
160+
}
131161
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.hjq.toast;
2+
3+
import java.lang.reflect.InvocationHandler;
4+
import java.lang.reflect.Method;
5+
6+
/**
7+
* author : Android 轮子哥
8+
* github : https://github.com/getActivity/ToastUtils
9+
* time : 2021/11/13
10+
* desc : 通知服务代理代理对象
11+
*/
12+
final class NotificationServiceProxy implements InvocationHandler {
13+
14+
/** 被代理的对象 */
15+
private final Object mSource;
16+
17+
public NotificationServiceProxy(Object source) {
18+
mSource = source;
19+
}
20+
21+
@Override
22+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
23+
switch (method.getName()) {
24+
case "enqueueToast":
25+
case "enqueueToastEx":
26+
case "cancelToast":
27+
// 将包名修改成系统包名,这样就可以绕过系统的拦截
28+
// 部分华为机将 enqueueToast 修改成了 enqueueToastEx
29+
args[0] = "android";
30+
break;
31+
default:
32+
break;
33+
}
34+
// 使用动态代理
35+
return method.invoke(mSource, args);
36+
}
37+
}

0 commit comments

Comments
 (0)