本文章已授權(quán)鴻洋微信公眾號(hào)轉(zhuǎn)載:Toast不顯示了?
吐司彈不出來(lái)完美的解決方案:Toaster,接下來(lái)讓我們來(lái)一步步開(kāi)始分析這個(gè)問(wèn)題是如何出現(xiàn),解決的過(guò)程,以及解決的方法
首先我們先看一下大廠(chǎng) APP 的彈吐司

疑問(wèn)
連吐司彈不出來(lái)的手機(jī)是個(gè)什么梗?
是少部分機(jī)型問(wèn)題還是大多數(shù)機(jī)型的問(wèn)題?
為什么關(guān)閉了通知欄權(quán)限彈不出來(lái)?
為什么有的機(jī)型可以彈有的卻不行?
解答
自從我的 Toaster 框架發(fā)布了之后,被問(wèn)最多的一個(gè)問(wèn)題,你的Toast框架關(guān)閉通知欄權(quán)限還能彈出來(lái)嗎?我心想這 Toast 跟通知欄扯不上啥關(guān)系吧,但是既然有人這樣問(wèn)了,也只能半信半疑了,于是我便拿了我的小米8還有紅米Note5進(jìn)行了測(cè)試,發(fā)現(xiàn)并沒(méi)有該問(wèn)題,于是我統(tǒng)一回復(fù),這個(gè)是兼容問(wèn)題,極少數(shù)機(jī)型才可能出現(xiàn)的問(wèn)題,為保證框架穩(wěn)定性,不給予兼容
于是還有人陸陸續(xù)續(xù)給我反饋了這個(gè)問(wèn)題,反饋的人都是用華為機(jī)型出現(xiàn)的問(wèn)題,我便開(kāi)始重視起來(lái),剛好有同事用的是華為 P9,我跟他借了一下手機(jī),一借不要緊,一借一下午。估計(jì)同事的內(nèi)心是崩潰的,因?yàn)檫@個(gè)問(wèn)題被 100% 復(fù)現(xiàn)了,真的關(guān)閉通知欄權(quán)限后吐司彈不出來(lái)了
于是我翻遍了 Toast 的源碼,吐司底層是 WindowManager 實(shí)現(xiàn)的,但是這跟通知欄權(quán)限有什么關(guān)系呢?就算有關(guān)系也是和 NotificationManager 有關(guān)系,到底和通知欄權(quán)限扯上啥關(guān)系了呢?經(jīng)過(guò)查看系統(tǒng)源碼發(fā)現(xiàn),吐司的創(chuàng)建是使用到了 WindowManager 去創(chuàng)建,但是顯示吐司的時(shí)候使用了 INotificationManager ,看類(lèi)名就知道肯定和 NotificationManager 有聯(lián)系,這就是為什么關(guān)閉了通知欄權(quán)限后導(dǎo)致了吐司顯示不出來(lái)的問(wèn)題
現(xiàn)在經(jīng)過(guò)測(cè)試,大部分小米機(jī)型不會(huì)因?yàn)橥ㄖ獧跈?quán)限被關(guān)閉而原生的Toast彈不出來(lái),而華為榮耀,三星等都會(huì)出現(xiàn)通知欄權(quán)限被關(guān)閉后導(dǎo)致原生Toast顯示不出來(lái),這可能是小米手機(jī)對(duì)這個(gè)吐司的顯示做了特殊處理,這個(gè)問(wèn)題在Github上排名前幾的Toast框架都會(huì)出現(xiàn),并且一些大廠(chǎng)的APP(除QQ微信和美團(tuán)外)也會(huì)出現(xiàn)該問(wèn)題
吐司彈不出來(lái)的后果
Toast是我們?nèi)粘i_(kāi)發(fā)中最常用的類(lèi),如果我們的APP在通知欄推送的消息比較多,用戶(hù)就會(huì)把我們的通知欄權(quán)限屏蔽了,但是這個(gè)會(huì)引起一個(gè)連帶反應(yīng),就是應(yīng)用中所有使用到 Toast 的地方都會(huì)顯示不出來(lái),徹底成為一個(gè)啞巴應(yīng)用,例如以下情景:
賬戶(hù)密碼輸入錯(cuò)誤,吐司彈不出來(lái)
用戶(hù)網(wǎng)絡(luò)支付失敗,吐司彈不出來(lái)
網(wǎng)絡(luò)請(qǐng)求錯(cuò)誤,吐司彈不出來(lái)
雙擊退出應(yīng)用,吐司彈不出來(lái)
等等情況,只要用到原生 Toast 都顯示不出來(lái)
其實(shí)這是一個(gè)系統(tǒng)的Bug,谷歌為了讓?xiě)?yīng)用的 Toast 能夠顯示在其他應(yīng)用上面,所以使用了通知欄相關(guān)的 API,但是這個(gè) API 隨著用戶(hù)屏蔽通知欄而變得不可用,系統(tǒng)錯(cuò)誤地認(rèn)為你沒(méi)有通知欄權(quán)限,從而間接導(dǎo)致 Toast 有 show 請(qǐng)求時(shí)被系統(tǒng)所攔截
Toast 源碼解析
首先看一下 Toast 的構(gòu)成

再看一下 Toast 內(nèi)部的 API

里面還有一個(gè)內(nèi)部類(lèi),再看一下內(nèi)部的 API

從這里我們不難推斷,Toast 只是一個(gè)外觀(guān)類(lèi),最終實(shí)現(xiàn)還是由其內(nèi)部類(lèi)來(lái)實(shí)現(xiàn),由于這個(gè)內(nèi)部類(lèi)太長(zhǎng),這里放一下這個(gè)內(nèi)部類(lèi)的源碼,簡(jiǎn)單過(guò)一遍就好
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
String mPackageName;
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
TN(String packageName, @Nullable Looper looper) {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
只需要稍微簡(jiǎn)單看一下就看懂,Toast 底層就是用這個(gè)內(nèi)部類(lèi)去實(shí)現(xiàn),請(qǐng)記住,這個(gè)內(nèi)部類(lèi)叫做 TN,字段名為 mTN,接下來(lái)先讓我們看一下 Toast 中 cancel 方法的源碼

cancel最終還是調(diào)用了內(nèi)部類(lèi) TN 中的同名方法,接下來(lái)再看 Toast 中 show 方法的源碼

仔細(xì)觀(guān)察的同學(xué)就會(huì)發(fā)現(xiàn)了,這個(gè) show 的方法可不是像 cancel 一樣只調(diào)用了 TN 內(nèi)部類(lèi)中的同名方法,還調(diào)用了 INotificationManager 這個(gè) API,其實(shí)不難發(fā)現(xiàn),這個(gè) INotificationManager 是系統(tǒng)的 AIDL,不信的話(huà)我們?cè)倏匆幌逻@個(gè) INotificationManager

我相信學(xué)過(guò) AIDL 的同學(xué)會(huì)明白,這里不再講 AIDL 相關(guān)知識(shí),如需了解請(qǐng)自行百度
重點(diǎn)講一下 INotificationManager,這個(gè) AIDL 由系統(tǒng)實(shí)現(xiàn)的一個(gè)類(lèi),不同系統(tǒng)這個(gè) AIDL 所對(duì)應(yīng)的類(lèi)也不相同,這就充分說(shuō)明了為什么導(dǎo)致小米的機(jī)型關(guān)閉了通知欄權(quán)限還可以顯示,而華為就不行的原因,具體原因請(qǐng)?jiān)倏丛创a

因?yàn)檫@里傳了應(yīng)用的包名給系統(tǒng)通知欄,如果這個(gè)包名對(duì)應(yīng)的APP的通知欄權(quán)限被關(guān)閉了,吐司自然也就彈不出來(lái)了
那么該如何著手解決這個(gè)問(wèn)題
先思考一個(gè)問(wèn)題,Toast 顯示是使用了 INotificationManager,和通知欄有關(guān)系,而Toast 的創(chuàng)建是使用了 WindowManager,和通知欄沒(méi)有關(guān)系,那么我們可不可以通過(guò) WindowManager 的方式來(lái)創(chuàng)建類(lèi)似于 Toast 一樣的東西呢,答案也是可以的,只不過(guò)在過(guò)程中會(huì)遇到非常棘手的問(wèn)題,接下來(lái)讓我們解決這些遇到的問(wèn)題
首先創(chuàng)建一個(gè) WindowManager 需要 一個(gè) View 參數(shù)和 WindowManager.LayoutParams 參數(shù),這里說(shuō)一下 WindowManager.LayoutParams 的創(chuàng)建,直接復(fù)制 Toast 部分代碼
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
// 找不到 com.android.internal.R.style.Animation_Toast
// params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.windowAnimations = -1;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
然后使用 WindowManager 調(diào)用 addView 顯示,然后報(bào)了錯(cuò)
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
其原因在于我們使用了 type,為什么不能加 TYPE_TOAST,因?yàn)橥ㄖ獧?quán)限在關(guān)閉后設(shè)置顯示的類(lèi)型為T(mén)oast會(huì)報(bào)錯(cuò),所以這里我們把這句代碼注釋掉,然后就可以顯示出來(lái)了
params.type = WindowManager.LayoutParams.TYPE_TOAST;
WindowManager 沒(méi)有吐司的顯示效果
其原因在于我們復(fù)制了 Toast 的部分代碼,而其中的動(dòng)畫(huà)代碼引用了系統(tǒng) R 文件中資源,而我無(wú)法直接在 Java 代碼中引用
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
Java代碼不能引用這個(gè)Style不代表XML就不行,在這里創(chuàng)建一個(gè) Style 并且繼承原生 Toast 樣式,這里我們可以自定義,也可以直接使用系統(tǒng)的,為了和系統(tǒng)的樣式統(tǒng)一,這里就直接使用系統(tǒng)的
<style name="ToastAnimation" parent="@android:style/Animation.Toast">
<!--<item name="android:windowEnterAnimation">@anim/toast_enter</item>-->
<!--<item name="android:windowExitAnimation">@anim/toast_exit</item>-->
</style>
然后重新指定 params.windowAnimations 即可解決該問(wèn)題
params.windowAnimations = R.style.ToastAnimation;
WindowManager 沒(méi)有自動(dòng)消失的問(wèn)題
首先 WindowManager 并不能像 Toast 顯示后自動(dòng)消失,如果要像 Toast 一樣自動(dòng)消失很容易,在 WindowManager 顯示后發(fā)送一個(gè)定時(shí)關(guān)閉的任務(wù),那么問(wèn)題來(lái)了,這個(gè)顯示的時(shí)間如何定義?系統(tǒng) Toast 顯示的時(shí)間是什么樣子?首先我們需要先看一下 Toast 給我們提供的兩個(gè)常量值

從這張圖上我們并沒(méi)有發(fā)現(xiàn)什么有價(jià)值的東西,我們繼續(xù)往下找,看看是什么地方引用了這些常量

繼續(xù)通過(guò)查看源碼得知

但是通過(guò)測(cè)試,短吐司顯示的時(shí)長(zhǎng)為2-3秒,而長(zhǎng)吐司顯示的時(shí)長(zhǎng)是3-4秒,所以這兩個(gè)值并不是吐司顯示時(shí)長(zhǎng)的毫秒數(shù),那么我們?cè)撊绾蔚贸稣_的毫秒數(shù)呢?這個(gè)問(wèn)題就留給大家去思考,這里不做解答
只能使用當(dāng)前 Activity 創(chuàng)建 WindowManager 的缺陷
發(fā)現(xiàn)一個(gè)問(wèn)題,Activity 和 Application 同樣是 Context 的子類(lèi),如果使用 Activity 獲取的 WindowManager 對(duì)象可以創(chuàng)建出來(lái),但是如果使用 Application 獲取的 WindowManager 對(duì)象卻報(bào)了錯(cuò)
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
報(bào)錯(cuò)已經(jīng)說(shuō)得很清楚了,創(chuàng)建 WindowManager 不能使用 Application 對(duì)象去創(chuàng)建,也就是說(shuō)只能通過(guò) Activity 對(duì)象去創(chuàng)建 WindowManager
那么問(wèn)題來(lái)了,每次彈這種 “Toast” 需要當(dāng)前 Activity 對(duì)象,這個(gè)問(wèn)題對(duì)于常年使用框架的同學(xué)是致命的
這里以我做的框架 Toaster 為例子,顯示一個(gè)吐司是這樣子調(diào)用的
Toaster.show("我是吐司");
如果要解決在關(guān)閉通知欄權(quán)限后吐司還能再?gòu)棾鰜?lái)的問(wèn)題,就需要改成
Toaster.show(MainActivity.this, "我是吐司");
先說(shuō)一下這個(gè)問(wèn)題帶來(lái)的影響吧,我是框架的作者,對(duì)于我來(lái)說(shuō),只需要在 Toaster 中 show 方法多添加一個(gè) Activity 參數(shù)即可,但是對(duì)于使用框架的人,在更新完框架后,整個(gè)項(xiàng)目所有使用到這個(gè)Toaster.show()方法都會(huì)報(bào)錯(cuò),需要多傳入一個(gè)Activity 參數(shù),相信他們的內(nèi)心幾乎是崩潰的,那么有沒(méi)有一種好的辦法解決這個(gè)問(wèn)題,答案當(dāng)然是有了,可以用一個(gè)冷門(mén)的 API
Application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback);
這個(gè) API 是在 安卓 4.0 之后才有的,而現(xiàn)在大多數(shù)設(shè)備已經(jīng)在 安卓 5.0 及以上,所以這個(gè) API 還是有前途的,接下看一下 ActivityLifecycleCallbacks 這個(gè)接口有什么方法吧
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}
看到這里,相信各位已經(jīng)知道真相了,這個(gè)方法用于監(jiān)聽(tīng)?wèi)?yīng)用中 Activity 中的生命周期方法
那么我們就可以通過(guò)這個(gè) API 來(lái)獲取當(dāng)前和用戶(hù)交互的 Activity 對(duì)象,從而完成讓當(dāng)前 Activity 對(duì)象去創(chuàng)建 WindowManager
使用 WindowManager 實(shí)現(xiàn) Toast 出現(xiàn)局限性的問(wèn)題
當(dāng)然用 WindowManager 創(chuàng)建的 View 必然也會(huì)受 Activity 的限制,因?yàn)榫椭荒茱@示這個(gè) Activity 上,如果在其他界面上則會(huì)顯示不了,而系統(tǒng)原生的 Toast 則可以出現(xiàn)別的界面上,那有沒(méi)有什么解決辦法呢?
WindowManager 在沒(méi)有懸浮窗權(quán)限的時(shí)候就只能顯示依附于調(diào)用的 Activity,當(dāng)有授予了懸浮窗權(quán)限之后,可以通過(guò)改變type參數(shù)來(lái)更改 WindowManager 顯示范圍,可以讓這個(gè) WindowManager 顯示在其他界面之上,這樣 Toast 就不會(huì)隨著 Activity 的不可見(jiàn)而變得不可見(jiàn)
// 判斷是否為 Android 6.0 及以上系統(tǒng)并且有懸浮窗權(quán)限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(mToast.getView().getContext())) {
// 解決使用 WindowManager 創(chuàng)建的 Toast 只能顯示在當(dāng)前 Activity 的問(wèn)題
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}else {
params.type = WindowManager.LayoutParams.TYPE_PHONE;
}
}
如何在原生 Toast 和 WindowManager 中取舍
這樣我們比對(duì)一組數(shù)據(jù):
| 類(lèi)型 | 顯示范圍 | 需要參數(shù) | 兼容性 | 效率 | 通知欄權(quán)限 | 懸浮窗權(quán)限 |
|---|---|---|---|---|---|---|
| 原生 Toast | 所有界面 | Context子類(lèi) | 高 | 一般 | 需要 | 不需要 |
| WindowManager | 當(dāng)前Activity | Activity子類(lèi) | 一般 | 高 | 不需要 | 不需要 |
經(jīng)過(guò)對(duì)比,原生的 Toast 的優(yōu)勢(shì)還是要大于 WindowManager 的,所以如果在有在通知欄權(quán)限的前提下,建議使用原生的 Toast,我們可以通過(guò)判斷通知欄權(quán)限是否被關(guān)閉,來(lái)判斷是來(lái)顯示原生 Toast 還是 WindowManager,方法代碼如下:
/**
* 檢查通知欄權(quán)限有沒(méi)有開(kāi)啟
*/
public static boolean isNotificationEnabled(Context context){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
try {
Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
int value = (Integer) opPostNotificationValue.get(Integer.class);
return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
} catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
return true;
}
} else {
return true;
}
}
