解决Toast在APP通知关闭不显示

Android在8.0中针对通知一块在功能上做了较大的改动。
Toast被纳入到了通知管理,其实这在Android 4.4(API 19)以上就已经有特别的处理了。只是一般用户不会怎么注意,开发人员也不会很在意APP的通知开关,因为GOOGLE还没有在通知上大做文章,进入到APP信息中,通知的开关也不是很起眼。但是8.0上针对通知部分(主要针对下拉通知)做了较大修改,其中牵连到Toast,且APP信息也种通知开发内选项也丰富了。

因此在APP设置中关闭通知开关后,Toast也会有不显示的情况——这个主要是针对国内不同的厂商定制,表现各有不同。

标准API中可以通过

android.support.v4.app.NotificationManagerCompat

的方法

/**
 * Returns whether notifications from the calling package are not blocked.
 */
public boolean areNotificationsEnabled ()

判断通知是否阻止。

这个方法里的实现分成了3段,Android 4.4之前方法永远是true,在Android 4.4 ~ Android 7通过AppOpsManager来判断,在Android 7以上最终调用的android.app.NotificationManager进行的判断。

至于为什么关闭通知开关后不显示Toast,需要跟踪到NotificationManagerService的源码查看enqueueToast方法,这里不具体说,主要说说如何解决。


先看看各机型测试的情况

机型 Android版本 areNotificationsEnabled SDK_INT Toast类型 是否显示
OPPO R15 8.1.0 false 27 Customized Toast
MI NOTE LTE 6.0.1 false 23 Android OS Toast
NUBIA Z11 mini 5.1.1 false 22 Android OS Toast
HW MATE 7 6.0 false 23 Android OS Toast

小米显示TYPE_TOAST机型

机型 Android版本 MIUI版本 areNotificationEnabled SDK_INT Toast显示 是否显示 备注
MI MOTE LTE 6.0 MIUI 9.2稳定版 false 23 Custimized Toast 正常显示
Redmi 3s 6.0 MIUI 7.3稳定版 false 23 Customized Toast 正常显示
MI 4c 5.1.1 MIUI 8.2稳定版 false 22 Customized Toast 虽然显示,但是不正常,点击N次只显示一两次
MI 5 8.0.0 MIUI 10 8.11.22开发版 false 26 Customized Toast 正常显示
MI 8 9 MIUI 10.1稳定版 false 28 Customized Toast 正常显示
Redmi Note4 6.0 MIUI 10.2稳定版 false 23 Android OS Toast 正常显示

结果: MIUI 8以下Toast不显示,在MIUI论坛上可以看到这是因为MIUI不支持TYPE_TOAST,因此无法显示。

####小米,华为等关闭通知,系统Toast显示情况。

机型 Android版本 MIUI版本 areNotificationEnabled SDK_INT Toast显示 是否显示 备注
MI MOTE LTE 6.0 MIUI 9.2稳定版 false 23 Android OS Toast 正常显示
MI 3 4.4.4 MIUI 9.2稳定版 false 19 Android OS Toast 正常显示
MI 4C 5.1.1 MIUI 8.2稳定版 false 22 Android OS Toast -
MI 5 8.0.0 MIUI 10 8.00.22开发版 false 26 Android OS Toast -
MI 8 9 MIUI 10.1稳定版 false 28 Android OS Toast -
Redmi 3s 6.0.1 MIUI 7.3稳定版 false 23 Android OS Toast -
Redmi Note4 6.0 MIUI 10.2稳定版 false 23 Android OS Toast -
华为荣耀6 4.4.2 - false 19 Android OS Toast -
华为荣耀7 5.0.2 - false 21 Android OS Toast -
华为Mate 10 9 - false 28 Android OS Toast -
魅族 MX4 PRO 5.1.1 - false 22 Android OS Toast -
魅族 MX6 7.1.1 - false 25 Android OS Toast -
OPPO R15 8.1.0 - false 27 Android OS Toast -
OPPO R11 PLUS 8.1.0 - false 27 Android OS Toast -
OPPO R7 4.4.4 - false 19 Android OS Toast -
OPPO R9 PLUS 5.1.1 - false 22 Android OS Toast -
努比亚Z11 mini 5.1.1 - false 22 Android OS Toast -

MIUI显示与其他品牌手机Toast显示有区别。
OPPO R15也显示了Toast。

看看自定义Toast

根据系统Toast进行修改,由于系统Toast中调用了NotificationManagerService的接口,因此将涉及的部分先进行删除。第二,Toast在NotificationManagerService中是被进行排队处理,因此删除了NotificationManagerService的调用部分后,需要自己定义队列管理。

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import static android.os.Build.VERSION.SDK_INT;

/**
 * A toast is a view containing a quick little message for the user.  The toast class
 * helps you create and show those.
 *
 * 

* When the view is shown to the user, appears as a floating view over the * application. It will never receive focus. The user will probably be in the * middle of typing something else. The idea is to be as unobtrusive as * possible, while still showing the user the information you want them to see. * Two examples are the volume control, and the brief message saying that your * settings have been saved. *

* The easiest way to use this class is to call one of the static methods that constructs * everything you need and returns a new Toast object. * *

*

Developer Guides

*

For information about creating Toast notifications, read the * Toast Notifications developer * guide.

*
*/ class Toast { private static final String TAG = "Toast"; private static final boolean localLOGV = false; /** * 维护toast的队列 */ private static BlockingQueue mQueue = new LinkedBlockingQueue(); /** * 原子操作:判断当前是否在读取{**@linkplain **#mQueue 队列}来显示toast */ private static AtomicInteger mAtomicInteger = new AtomicInteger(0); @Retention(RetentionPolicy.SOURCE) @interface Duration { } /** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. * * @see #setDuration */ public static final int LENGTH_SHORT = 3000; /** * Show the view or text notification for a long period of time. This time * could be user-definable. * * @see #setDuration */ public static final int LENGTH_LONG = 5000; private final Context mContext; private final TN mTN; private long mDuration; private View mNextView; private final static Handler mHandler = new Handler(); private static Runnable mActive = new Runnable() { @Override public void run() { activeQueue(); } }; private static void activeQueue() { final TN tn = mQueue.peek(); if (tn == null) { mAtomicInteger.decrementAndGet(); return; } mHandler.post(new Runnable() { @Override public void run() { tn.show(); } }); mHandler.postDelayed(new Runnable() { @Override public void run() { if (tn.mNextView != null && tn.mNextView.getParent() != null) { mQueue.poll(); } tn.hide(); } }, tn.mDuration); mHandler.postDelayed(mActive, tn.mDuration); } /** * Construct an empty Toast object. You must call {@link #setView} before you * can call {@link #show}. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. */ public Toast(Context context) { this(context, null); } /** * Constructs an empty Toast object. If looper is null, Looper.myLooper() is used. */ public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; mTN = new TN(context.getPackageName(), looper); mTN.mY = dip2px(context, 26); mTN.mGravity = 0x00000051; } public int dip2px(Context context,float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale +0.5f); } /** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } TN tn = mTN; tn.mNextView = mNextView; mQueue.offer(tn); if (mAtomicInteger.get() == 0) { mAtomicInteger.incrementAndGet(); mHandler.post(mActive); } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void cancel() { if (0 == mAtomicInteger.get() && mQueue.isEmpty()) { return; } final TN tn = mQueue.peek(); if (mTN == tn) { mHandler.removeCallbacks(mActive); mHandler.post(new Runnable() { @Override public void run() { if (tn != null) { tn.cancel(); } } }); mHandler.post(mActive); } } /** * Set the view to show. * * @see #getView */ public void setView(View view) { mNextView = view; } /** * Return the view. * * @see #setView */ public View getView() { return mNextView; } /** * Set how long to show the view for. * * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(@Duration int duration) { mDuration = duration; mTN.mDuration = duration; } /** * Return the duration. * * @see #setDuration */ @Duration public long getDuration() { return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin) { mTN.mHorizontalMargin = horizontalMargin; mTN.mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin() { return mTN.mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin() { return mTN.mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset) { mTN.mGravity = gravity; mTN.mX = xOffset; mTN.mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * * @see android.view.Gravity * @see #getGravity */ public int getGravity() { return mTN.mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset() { return mTN.mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset() { return mTN.mY; } /** * Gets the LayoutParams for the Toast window. * * @hide */ public WindowManager.LayoutParams getWindowParams() { return mTN.mParams; } /** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} */ public static Toast makeText(Context context, CharSequence text, @Duration int duration) { return makeText(context, null, text, duration); } /** * Make a standard toast to display using the specified looper. * If looper is null, Looper.myLooper() is used. * * @hide */ @SuppressLint("ShowToast") public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { Toast result = new Toast(context, looper); View v = android.widget.Toast.makeText(context, text, duration).getView(); TextView tv = v.findViewById(android.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; } /** * Make a standard toast that just contains a text view with the text from a resource. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * @throws Resources.NotFoundException if the resource can't be found. */ public static Toast makeText(Context context, @StringRes int resId, @Duration int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } /** * Update the text in a Toast that was previously created using one of the makeText() methods. * * @param resId The new text for the Toast. */ public void setText(@StringRes int resId) { setText(mContext.getText(resId)); } /** * Update the text in a Toast that was previously created using one of the makeText() methods. * * @param s The new text for the Toast. */ public void setText(CharSequence s) { if (mNextView == null) { throw new RuntimeException("This Toast was not created with Toast.makeText()"); } TextView tv = mNextView.findViewById(android.R.id.message); if (tv == null) { throw new RuntimeException("This Toast was not created with Toast.makeText()"); } tv.setText(s); } private static class TN { 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; int mY; float mHorizontalMargin; float mVerticalMargin; View mView; View mNextView; long mDuration = SHORT_DURATION_TIMEOUT; WindowManager mWM; String mPackageName; static final long SHORT_DURATION_TIMEOUT = 2000; static final long LONG_DURATION_TIMEOUT = 5000; 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 = android.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: { handleShow(); 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; break; } } } }; } /** * schedule handleShow into the right thread */ public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW).sendToTarget(); } /** * schedule handleHide into the right thread */ 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() { 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 = mPackageName; 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(); int gravity = mGravity; if (SDK_INT > 16) { 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; if (SDK_INT > 23) { try { Class clazz = Class.forName("android.view.WindowManager"); Field field = clazz.getDeclaredField("hideTimeoutMilliseconds"); field.setAccessible(true); field.setLong(mParams, mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } 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); } catch (WindowManager.BadTokenException e) { /* ignore */ } } } 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; } } } }

这个类完全可用,需要使用可以拷贝直接使用

针对MIUI 8需要在WindowManager.addView前后添加标记设置。

                //...
                try {
                    final int miuiVersion = MobileManufacturer.miuiVersion();
                    if (miuiVersion <= 8) {
                        MobileManufacturer.setMIUIInternational(true);
                    }
                    mWM.addView(mView, mParams);
                    if (miuiVersion <= 8) {
                        MobileManufacturer.setMIUIInternational(false);
                    }
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
               // ...

针对不同厂商需要可能需要不同适配,因此重新定义一个判断厂商工具类。

// MobileManufacturer.java
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.TextUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

final class MobileManufacturer {
    static boolean isMIUI() {
        return TextUtils.equals(Build.MANUFACTURER, "Xiaomi");
    }

    static int miuiVersion() {
        return getMIUIVersion();
    }

    static void setMIUIInternational(boolean flag) {
        try {
            final Class BuildForMi = Class.forName("miui.os.Build");
            final Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD");
            isInternational.setAccessible(true);
            isInternational.setBoolean(null, flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static int getMIUIVersion() {
        try {
            @SuppressLint("PrivateApi")
            Class sysClass = Class.forName("android.os.SystemProperties");
            Method getStringMethod = sysClass.getDeclaredMethod("get", String.class);
            final String version = (String) getStringMethod.invoke(sysClass, "ro.miui.ui.version.name");
            if (!TextUtils.isEmpty(version)) {
                return Integer.valueOf(version.substring(1));

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 6;
    }
}

这里主要采用了MIUI做实验对比,若不同厂商定制的手机不同而导致不显示Toast,均可采用以上自定义的Toast进行显示。

查看厂商ROM

你可能感兴趣的:(Android移动开发)