Android 悬浮窗各机型各系统适配大全

丈夫志四海,万里犹比邻。这篇文章主要讲述Android 悬浮窗各机型各系统适配大全相关的知识,希望能为你提供帮助。
这篇博客主要介绍的是 android 主流各种机型和各种版本的悬浮窗权限适配, 但是由于碎片化的问题, 所以在适配方面也无法做到完全的主流机型适配, 这个需要大家的一起努力, 这个博客的名字永远都是一个将来时, 感兴趣或者找到其他机型适配方法的请留言告诉我, 或者加群544645972一起交流一下, 非常感谢~
相关权限请看我的另一篇博客: android permission权限与安全机制解析( 下) , 或者关于权限的案例使用: android WindowManager解析与骗取QQ密码案例分析, 还有录音和摄像头权限的适配: Android 录音和摄像头权限适配。
转载请注明出处: http://blog.csdn.net/self_study/article/details/52859790。
源码会实时更新在 gitHub 上, 不会实时更新博客, 所以想要看最新代码的同学, 请直接去 github 页面查看 markdown。
悬浮窗适配 悬浮窗适配有两种方法: 第一种是按照正规的流程, 如果系统没有赋予 APP 弹出悬浮窗的权限, 就先跳转到权限授权界面, 等用户打开该权限之后, 再去弹出悬浮窗, 比如 QQ 等一些主流应用就是这么做得; 第二种就是利用系统的漏洞, 绕过权限的申请, 简单粗暴, 这种方法我不是特别建议, 但是现在貌似有些应用就是这样, 比如 UC 和有道词典, 这样适配在大多数手机上都是 OK 的, 但是在一些特殊的机型不行, 比如某米的 miui8。
正常适配流程 在 4.4~5.1.1 版本之间, 和 6.0~最新版本之间的适配方法是不一样的, 之前的版本由于 google 并没有对这个权限进行单独处理, 所以是各家手机厂商根据需要定制的, 所以每个权限的授权界面都各不一样, 适配起来难度较大, 6.0 之后适配起来就相对简单很多了。
Android 4.4 ~ Android 5.1.1
由于判断权限的类 AppOpsManager 是 API19 版本添加, 所以Android 4.4 之前的版本( 不包括4.4) 就不用去判断了, 直接调用 WindowManager 的 addView 方法弹出即可, 但是貌似有些特殊的手机厂商在 API19 版本之前就已经自定义了悬浮窗权限, 如果有发现的, 请联系我。
众所周知, 国产手机的种类实在是过于丰富, 而且一个品牌的不同版本还有不一样的适配方法, 比如某米( 嫌弃脸) , 所以我在实际适配的过程中总结了几种通用的方法, 大家可以参考一下:

  • 直接百度一下, 搜索关键词“小米手机悬浮窗适配”等;
  • 看看 QQ 或者其他的大公司 APP 是否已经适配, 如果已经适配, 跳转到相关权限授权页面之后, 或者自己能够直接在设置里找到悬浮窗权限授权页面也是一个道理, 使用 adb shell dumpsys activity 命令, 找到相关的信息, 如下图所示
    Android 悬浮窗各机型各系统适配大全

    文章图片

    可以清楚看到授权 activity 页面的包名和 activity 名, 而且可以清楚地知道跳转的 intent 是否带了 extra, 如果没有 extra 就可以直接跳入, 如果带上了 extra, 百度一下该 activity 的名字, 看能否找到有用信息, 比如适配方案或者源码 APK 之类的;
  • 依旧利用上面的方法, 找到 activity 的名字, 然后 root 准备适配的手机, 直接在相关目录 /system/app 下把源码 APK 拷贝出来, 反编译, 根据 activity 的名字找到相关代码, 之后的事情就简单了;
  • 还有一个方法就是发动人力资源去找, 看看已经适配该手机机型的 app 公司是否有自己认识的人, 或者干脆点, 直接找这个手机公司里面是否有自己认识的手机开发朋友, 直接询问, 方便快捷。

常规手机 由于 6.0 之前的版本常规手机并没有把悬浮窗权限单独拿出来, 所以正常情况下是可以直接使用 WindowManager.addView 方法直接弹出悬浮窗。
如何判断手机的机型, 办法很多, 在这里我就不贴代码了, 一般情况下在 terminal 中执行 getprop 命令, 然后在打印出来的信息中找到相关的机型信息即可, 这里贴出国产几款常见机型的判断:
/** * 获取 emui 版本号 * @ return */ public static double getEmuiVersion() { try { String emuiVersion = getSystemProperty(" ro.build.version.emui" ); String version = emuiVersion.substring(emuiVersion.indexOf(" _" ) + 1); return Double.parseDouble(version); } catch (Exception e) { e.printStackTrace(); } return 4.0; }/** * 获取小米 rom 版本号, 获取失败返回 -1 * * @ return miui rom version code, if fail , return -1 */ public static int getMiuiVersion() { String version = getSystemProperty(" ro.miui.ui.version.name" ); if (version != null) { try { return Integer.parseInt(version.substring(1)); } catch (Exception e) { Log.e(TAG, " get miui version code error, version : " + version); } } return -1; }public static String getSystemProperty(String propName) { String line; BufferedReader input = null; try { Process p = Runtime.getRuntime().exec(" getprop " + propName); input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); line = input.readLine(); input.close(); } catch (IOException ex) { Log.e(TAG, " Unable to read sysprop " + propName, ex); return null; } finally { if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, " Exception while closing InputStream" , e); } } } return line; } public static boolean checkIsHuaweiRom() { return Build.MANUFACTURER.contains(" HUAWEI" ); }/** * check if is miui ROM */ public static boolean checkIsMiuiRom() { return !TextUtils.isEmpty(getSystemProperty(" ro.miui.ui.version.name" )); }public static boolean checkIsMeizuRom() { //return Build.MANUFACTURER.contains(" Meizu" ); String meizuFlymeOSFlag= getSystemProperty(" ro.build.display.id" ); if (TextUtils.isEmpty(meizuFlymeOSFlag)){ return false; }else if (meizuFlymeOSFlag.contains(" flyme" ) || meizuFlymeOSFlag.toLowerCase().contains(" flyme" )){ returntrue; }else { return false; } }/** * check if is 360 ROM */ public static boolean checkIs360Rom() { return Build.MANUFACTURER.contains(" QiKU" ); }

小米 首先需要适配的就应该是小米了, 而且比较麻烦的事情是, miui 的每个版本适配方法都是不一样的, 所以只能每个版本去单独适配, 不过还好由于使用的人数多, 网上的资料也比较全。首先第一步当然是判断是否赋予了悬浮窗权限, 这个时候就需要使用到 AppOpsManager 这个类了, 它里面有一个 checkop 方法:
/** * Do a quick check for whether an application might be able to perform an operation. * This is < em> not< /em> a security check; you must use {@ link #noteOp(int, int, String)} * or {@ link #startOp(int, int, String)} for your actual security checks, which also * ensure that the given uid and package name are consistent.This function can just be * used for a quick check to see if an operation has been disabled for the application, * as an early reject of some work.This does not modify the time stamp or other data * about the operation. * @ param op The operation to check.One of the OP_* constants. * @ param uid The user id of the application attempting to perform the operation. * @ param packageName The name of the application attempting to perform the operation. * @ return Returns {@ link #MODE_ALLOWED} if the operation is allowed, or * {@ link #MODE_IGNORED} if it is not allowed and should be silently ignored (without * causing the app to crash). * @ throws SecurityException If the app has been configured to crash on this op. * @ hide */ public int checkOp(int op, int uid, String packageName) { try { int mode = mService.checkOperation(op, uid, packageName); if (mode = = MODE_ERRORED) { throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); } return mode; } catch (RemoteException e) { } return MODE_IGNORED; }

找到悬浮窗权限的 op 值是:
/** @ hide */ public static final int OP_SYSTEM_ALERT_WINDOW = 24;

注意到这个函数和这个值其实都是 hide 的, 所以没办法, 你懂的, 只能用反射:
/** * 检测 miui 悬浮窗权限 */ public static boolean checkFloatWindowPermission(Context context) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; } else { //if ((context.getApplicationInfo().flags & 1 < < 27) = = 1) { //return true; //} else { //return false; //} return true; } }@ TargetApi(Build.VERSION_CODES.KITKAT) private static boolean checkOp(Context context, int op) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { Class clazz = AppOpsManager.class; Method method = clazz.getDeclaredMethod(" checkOp" , int.class, int.class, String.class); return AppOpsManager.MODE_ALLOWED = = (int)method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } else { Log.e(TAG, " Below API 19 cannot invoke!" ); } return false; }

检测完成之后就是跳转到授权页面去开启权限了, 但是由于 miui 不同版本的权限授权页面不一样, 所以需要根据不同版本进行不同处理:
/** * 获取小米 rom 版本号, 获取失败返回 -1 * * @ return miui rom version code, if fail , return -1 */ public static int getMiuiVersion() { String version = RomUtils.getSystemProperty(" ro.miui.ui.version.name" ); if (version != null) { try { return Integer.parseInt(version.substring(1)); } catch (Exception e) { Log.e(TAG, " get miui version code error, version : " + version); Log.e(TAG, Log.getStackTraceString(e)); } } return -1; }/** * 小米 ROM 权限申请 */ public static void applyMiuiPermission(Context context) { int versionCode = getMiuiVersion(); if (versionCode = = 5) { goToMiuiPermissionActivity_V5(context); } else if (versionCode = = 6) { goToMiuiPermissionActivity_V6(context); } else if (versionCode = = 7) { goToMiuiPermissionActivity_V7(context); } else if (versionCode = = 8) { goToMiuiPermissionActivity_V8(context); } else { Log.e(TAG, " this is a special MIUI rom version, its version code " + versionCode); } }private static boolean isIntentAvailable(Intent intent, Context context) { if (intent = = null) { return false; } return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; }/** * 小米 V5 版本 ROM权限申请 */ public static void goToMiuiPermissionActivity_V5(Context context) { Intent intent = null; String packageName = context.getPackageName(); intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts(" package" , packageName, null); intent.setData(uri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } else { Log.e(TAG, " intent is not available!" ); }//设置页面在应用详情页面 //Intent intent = new Intent(" miui.intent.action.APP_PERM_EDITOR" ); //PackageInfo pInfo = null; //try { //pInfo = context.getPackageManager().getPackageInfo //(HostInterfaceManager.getHostInterface().getApp().getPackageName(), 0); //} catch (PackageManager.NameNotFoundException e) { //AVLogUtils.e(TAG, e.getMessage()); //} //intent.setClassName(" com.android.settings" , " com.miui.securitycenter.permission.AppPermissionsEditor" ); //intent.putExtra(" extra_package_uid" , pInfo.applicationInfo.uid); //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //if (isIntentAvailable(intent, context)) { //context.startActivity(intent); //} else { //AVLogUtils.e(TAG, " Intent is not available!" ); //} }/** * 小米 V6 版本 ROM权限申请 */ public static void goToMiuiPermissionActivity_V6(Context context) { Intent intent = new Intent(" miui.intent.action.APP_PERM_EDITOR" ); intent.setClassName(" com.miui.securitycenter" , " com.miui.permcenter.permissions.AppPermissionsEditorActivity" ); intent.putExtra(" extra_pkgname" , context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } else { Log.e(TAG, " Intent is not available!" ); } }/** * 小米 V7 版本 ROM权限申请 */ public static void goToMiuiPermissionActivity_V7(Context context) { Intent intent = new Intent(" miui.intent.action.APP_PERM_EDITOR" ); intent.setClassName(" com.miui.securitycenter" , " com.miui.permcenter.permissions.AppPermissionsEditorActivity" ); intent.putExtra(" extra_pkgname" , context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } else { Log.e(TAG, " Intent is not available!" ); } }/** * 小米 V8 版本 ROM权限申请 */ public static void goToMiuiPermissionActivity_V8(Context context) { Intent intent = new Intent(" miui.intent.action.APP_PERM_EDITOR" ); intent.setClassName(" com.miui.securitycenter" , " com.miui.permcenter.permissions.PermissionsEditorActivity" ); intent.putExtra(" extra_pkgname" , context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } else { Log.e(TAG, " Intent is not available!" ); } }

getSystemProperty 方法是直接调用 getprop 方法来获取系统信息:
public static String getSystemProperty(String propName) { String line; BufferedReader input = null; try { Process p = Runtime.getRuntime().exec(" getprop " + propName); input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); line = input.readLine(); input.close(); } catch (IOException ex) { Log.e(TAG, " Unable to read sysprop " + propName, ex); return null; } finally { if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, " Exception while closing InputStream" , e); } } } return line; }

最新的 V8 版本有些机型已经是 6.0 , 所以就是下面介绍到 6.0 的适配方法了, 感谢 @ pinocchio2mx 的反馈, 有些机型的 miui8 版本还是5.1.1, 所以 miui8 依旧需要做适配, 非常感谢, 希望大家一起多多反馈问题, 谢谢~~。
魅族 魅族的适配, 由于我司魅族的机器相对较少, 所以只适配了 flyme5.1.1/android 5.1.1 版本 mx4 pro 的系统。和小米一样, 首先也要通过 API19 版本添加的 AppOpsManager 类判断是否授予了权限:
/** * 检测 meizu 悬浮窗权限 */ public static boolean checkFloatWindowPermission(Context context) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; } return true; }@ TargetApi(Build.VERSION_CODES.KITKAT) private static boolean checkOp(Context context, int op) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { Class clazz = AppOpsManager.class; Method method = clazz.getDeclaredMethod(" checkOp" , int.class, int.class, String.class); return AppOpsManager.MODE_ALLOWED = = (int)method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } else { Log.e(TAG, " Below API 19 cannot invoke!" ); } return false; }

然后是跳转去悬浮窗权限授予界面:
/** * 去魅族权限申请页面 */ public static void applyPermission(Context context){ Intent intent = new Intent(" com.meizu.safe.security.SHOW_APPSEC" ); intent.setClassName(" com.meizu.safe" , " com.meizu.safe.security.AppSecActivity" ); intent.putExtra(" packageName" , context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }

如果有魅族其他版本的适配方案, 请联系我。
华为 华为的适配是根据网上找的方案, 外加自己的一些优化而成, 但是由于华为手机的众多机型, 所以覆盖的机型和系统版本还不是那么全面, 如果有其他机型和版本的适配方案, 请联系我, 我更新到 github 上。和小米, 魅族一样, 首先通过 AppOpsManager 来判断权限是否已经授权:
/** * 检测 Huawei 悬浮窗权限 */ public static boolean checkFloatWindowPermission(Context context) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; } return true; }@ TargetApi(Build.VERSION_CODES.KITKAT) private static boolean checkOp(Context context, int op) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { Class clazz = AppOpsManager.class; Method method = clazz.getDeclaredMethod(" checkOp" , int.class, int.class, String.class); return AppOpsManager.MODE_ALLOWED = = (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } else { Log.e(TAG, " Below API 19 cannot invoke!" ); } return false; }

然后根据不同的机型和版本跳转到不同的页面:
/** * 去华为权限申请页面 */ public static void applyPermission(Context context) { try { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //ComponentName comp = new ComponentName(" com.huawei.systemmanager" ," com.huawei.permissionmanager.ui.MainActivity" ); //华为权限管理 //ComponentName comp = new ComponentName(" com.huawei.systemmanager" , //" com.huawei.permissionmanager.ui.SingleAppActivity" ); //华为权限管理, 跳转到指定app的权限管理位置需要华为接口权限, 未解决 ComponentName comp = new ComponentName(" com.huawei.systemmanager" , " com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity" ); //悬浮窗管理页面 intent.setComponent(comp); if (RomUtils.getEmuiVersion() = = 3.1) { //emui 3.1 的适配 context.startActivity(intent); } else { //emui 3.0 的适配 comp = new ComponentName(" com.huawei.systemmanager" , " com.huawei.notificationmanager.ui.NotificationManagmentActivity" ); //悬浮窗管理页面 intent.setComponent(comp); context.startActivity(intent); } } catch (SecurityException e) { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //ComponentName comp = new ComponentName(" com.huawei.systemmanager" ," com.huawei.permissionmanager.ui.MainActivity" ); //华为权限管理 ComponentName comp = new ComponentName(" com.huawei.systemmanager" , " com.huawei.permissionmanager.ui.MainActivity" ); //华为权限管理, 跳转到本app的权限管理页面,这个需要华为接口权限, 未解决 //ComponentName comp = new ComponentName(" com.huawei.systemmanager" ," com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity" ); //悬浮窗管理页面 intent.setComponent(comp); context.startActivity(intent); Log.e(TAG, Log.getStackTraceString(e)); } catch (ActivityNotFoundException e) { /** * 手机管家版本较低 HUAWEI SC-UL10 */ //Toast.makeText(MainActivity.this, " act找不到" , Toast.LENGTH_LONG).show(); Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ComponentName comp = new ComponentName(" com.Android.settings" , " com.android.settings.permission.TabItem" ); //权限管理页面 android4.4 //ComponentName comp = new ComponentName(" com.android.settings" ," com.android.settings.permission.single_app_activity" ); //此处可跳转到指定app对应的权限管理页面, 但是需要相关权限, 未解决 intent.setComponent(comp); context.startActivity(intent); e.printStackTrace(); Log.e(TAG, Log.getStackTraceString(e)); } catch (Exception e) { //抛出异常时提示信息 Toast.makeText(context, " 进入设置页面失败, 请手动设置" , Toast.LENGTH_LONG).show(); Log.e(TAG, Log.getStackTraceString(e)); } }

emui4 之后就是 6.0 版本了, 按照下面介绍的 6.0 适配方案即可。
360 360手机的适配方案在网上可以找到的资料很少, 唯一可以找到的就是这篇: 奇酷360 手机中怎么跳转安全中心中指定包名App的权限管理页面, 但是博客中也没有给出最后的适配方案, 不过最后居然直接用最简单的办法就能跳进去了, 首先是权限的检测:
/** * 检测 360 悬浮窗权限 */ public static boolean checkFloatWindowPermission(Context context) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; } return true; }@ TargetApi(Build.VERSION_CODES.KITKAT) private static boolean checkOp(Context context, int op) { final int version = Build.VERSION.SDK_INT; if (version > = 19) { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); try { Class clazz = AppOpsManager.class; Method method = clazz.getDeclaredMethod(" checkOp" , int.class, int.class, String.class); return AppOpsManager.MODE_ALLOWED = = (int)method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } else { Log.e(" " , " Below API 19 cannot invoke!" ); } return false; }

如果没有授予悬浮窗权限, 就跳转去权限授予界面:
public static void applyPermission(Context context) { Intent intent = new Intent(); intent.setClassName(" com.android.settings" , " com.android.settings.Settings$OverlaySettingsActivity" ); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }

【Android 悬浮窗各机型各系统适配大全】哈哈哈, 是不是很简单, 有时候真相往往一点也不复杂, OK, 适配完成。
Android 6.0 及之后版本
我在博客android permission权限与安全机制解析( 下) - SYSTEM_ALERT_WINDOW中已经介绍到了适配方案, 悬浮窗权限在 6.0 之后就被 google 单独拿出来管理了, 好处就是对我们来说适配就非常方便了, 在所有手机和 6.0 以及之后的版本上适配的方法都是一样的, 首先要在 Manifest 中静态申请< uses-permission android:name= " android.permission.SYSTEM_ALERT_WINDOW" /> 权限, 然后在使用时先判断该权限是否已经被授权, 如果没有授权使用下面这段代码进行动态申请:
private static final int REQUEST_CODE = 1; //判断权限 private boolean commonROMPermissionCheck(Context context) { Boolean result = true; if (Build.VERSION.SDK_INT > = 23) { try { Class clazz = Settings.class; Method canDrawOverlays = clazz.getDeclaredMethod(" canDrawOverlays" , Context.class); result = (Boolean) canDrawOverlays.invoke(null, context); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } return result; }//申请权限 private void requestAlertWindowPermission() { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); intent.setData(Uri.parse(" package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE); }@ Override //处理回调 protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode = = REQUEST_CODE) { if (Settings.canDrawOverlays(this)) { Log.i(LOGTAG, " onActivityResult granted" ); } } }

上述代码需要注意的是:
  • 使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION 启动隐式Intent;
  • 使用 “package:” + getPackageName() 携带App的包名信息;
  • 使用 Settings.canDrawOverlays 方法判断授权结果。
在用户开启相关权限之后才能使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR ,要不然是会直接崩溃的哦。
特殊适配流程 如何绕过系统的权限检查, 直接弹出悬浮窗? android WindowManager解析与骗取QQ密码案例分析这篇博客中我已经指明出来了, 需要使用mParams.type = WindowManager.LayoutParams.TYPE_TOAST; 来取代 mParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; , 这样就可以达到不申请权限, 而直接弹出悬浮窗, 至于原因嘛, 我们看看 PhoneWindowManager 源码的关键处:
@ Override public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) { .... switch (type) { case TYPE_TOAST: // XXX right now the app process has complete control over // this...should introduce a token to let the system // monitor/control what they are doing. outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW; break; case TYPE_DREAM: case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: case TYPE_PRIVATE_PRESENTATION: case TYPE_VOICE_INTERACTION: case TYPE_ACCESSIBILITY_OVERLAY: // The window manager will check these. break; case TYPE_PHONE: case TYPE_PRIORITY_PHONE: case TYPE_SYSTEM_ALERT: case TYPE_SYSTEM_ERROR: case TYPE_SYSTEM_OVERLAY: permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW; outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW; break; default: permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; } if (permission != null) { if (permission = = android.Manifest.permission.SYSTEM_ALERT_WINDOW) { final int callingUid = Binder.getCallingUid(); // system processes will be automatically allowed privilege to draw if (callingUid = = Process.SYSTEM_UID) { return WindowManagerGlobal.ADD_OKAY; }// check if user has enabled this operation. SecurityException will be thrown if // this app has not been allowed by the user final int mode = mAppOpsManager.checkOp(outAppOp[0], callingUid, attrs.packageName); switch (mode) { case AppOpsManager.MODE_ALLOWED: case AppOpsManager.MODE_IGNORED: // although we return ADD_OKAY for MODE_IGNORED, the added window will // actually be hidden in WindowManagerService return WindowManagerGlobal.ADD_OKAY; case AppOpsManager.MODE_ERRORED: return WindowManagerGlobal.ADD_PERMISSION_DENIED; default: // in the default mode, we will make a decision here based on // checkCallingPermission() if (mContext.checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerGlobal.ADD_PERMISSION_DENIED; } else { return WindowManagerGlobal.ADD_OKAY; } } }if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { return WindowManagerGlobal.ADD_PERMISSION_DENIED; } } return WindowManagerGlobal.ADD_OKAY; }

从源码中可以看到, 其实 TYPE_TOAST 没有做权限检查, 直接返回了 WindowManagerGlobal.ADD_OKAY, 所以呢, 这就是为什么可以绕过权限的原因。还有需要注意的一点是 addView 方法中会调用到 mPolicy.adjustWindowParamsLw(win.mAttrs); , 这个方法在不同的版本有不同的实现:
//Android 2.0 - 2.3.7 PhoneWindowManager public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: case TYPE_TOAST: // These types of windows can' t receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; break; } }//Android 4.0.1 - 4.3.1 PhoneWindowManager public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: case TYPE_TOAST: // These types of windows can' t receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; attrs.flags & = ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; break; } }//Android 4.4 PhoneWindowManager @ Override public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: // These types of windows can' t receive input events. attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; attrs.flags & = ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; break; } }

可以看到, 在4.0.1以前, 当我们使用 TYPE_TOAST, Android 会偷偷给我们加上 FLAG_NOT_FOCUSABLE 和 FLAG_NOT_TOUCHABLE, 4.0.1 开始, 会额外再去掉FLAG_WATCH_OUTSIDE_TOUCH, 这样真的是什么事件都没了。而 4.4 开始, TYPE_TOAST 被移除了, 所以从 4.4 开始, 使用 TYPE_TOAST 的同时还可以接收触摸事件和按键事件了, 而4.4以前只能显示出来, 不能交互, 所以 API18 及以下使用 TYPE_TOAST 是无法接收触摸事件的, 但是幸运的是除了 miui 之外, 这些版本可以直接在 Manifest 文件中声明 android.permission.SYSTEM_ALERT_WINDOW权限, 然后直接使用 WindowManager.LayoutParams.TYPE_PHONE 或者 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 都是可以直接弹出悬浮窗的。
还有一个需要提到的是 TYPE_APPLICATION, 这个 type 是配合 Activity 在当前 APP 内部使用的, 也就是说, 回到 Launcher 界面, 这个悬浮窗是会消失的。
虽然这种方法确确实实可以绕过权限, 至于适配的坑呢, 有人遇到之后可以联系我, 我会持续完善。不过由于这样可以不申请权限就弹出悬浮窗, 而且在最新的 6.0+ 系统上也没有修复, 所以如果这个漏洞被滥用, 就会造成一些意想不到的后果, 因此我个人倾向于使用 QQ 的适配方案, 也就是上面的正常适配流程去处理这个权限。
更新: 7.1.1之后版本
最新发现在 7.1.1 版本之后使用 type_toast 重复添加两次悬浮窗, 第二次会崩溃, 跑出来下面的错误:
E/AndroidRuntime: FATAL EXCEPTION: main android.view.WindowManager$BadTokenException: Unable to add window -- window android.view.ViewRootImpl$W@ d7a4e96 has already been added at android.view.ViewRootImpl.setView(ViewRootImpl.java:691) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at com.tencent.ysdk.module.icon.impl.a.g(Unknown Source) at com.tencent.ysdk.module.icon.impl.floatingviews.q.onAnimationEnd(Unknown Source) at android.view.animation.Animation$3.run(Animation.java:381) at android.os.Handler.handleCallback(Handler.java:751) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6119) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

去追溯源码, 发现是这里抛出来的错误:
try { mOrigWindowType = mWindowAttributes.type; mAt

    推荐阅读