Toast通知欄權(quán)限填坑指南

本文章已授權(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;
    }
}

詳細(xì)的源碼地址請(qǐng)戳這里

Android 技術(shù)討論 Q 群:10047167

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容