Android 悬浮窗及权限

引言:需要实现一个视频悬浮播放的功能,功能实现后发现悬浮权限的检测与申请并没有想象中那样简单。

时间:2017年04月10日23:44:46

作者:JustDo23

01. 前言

看到悬浮窗最先想到的就是360悬浮小球和视频播放的悬浮小窗,悬浮功能通过WindowManager来实现,另外悬浮功能需要使用到相关的悬浮窗(SYSTEM_ALERT_WINDOW)权限,在 Android 6.0 之前 Google 并没有对这个权限进行单独处理,国内各个手机厂商订制 ROOM 进行后授权界面各不相同,悬浮权限的检测与申请的适配问题还需注意一下。

02. 悬浮窗口

利用WindowManager来实现悬浮小球等需求

  1. android悬浮窗口的实现
  2. Android桌面悬浮窗效果实现,仿360手机卫士悬浮窗效果
  3. Android桌面悬浮窗进阶,QQ手机管家小火箭效果实现
  4. 像360悬浮窗那样,用WindowManager实现炫酷的悬浮迷你音乐盒(上)
  5. 像360悬浮窗那样,用WindowManager实现炫酷的悬浮迷你音乐盒(下)

03. 悬浮权限

实现悬浮需要在功能清单中添加权限


这在 Android 中是一个特殊权限,Android 中的权限有一般权限、危险权限和两个特殊权限。

04. 权限适配

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

https://github.com/zhaozepeng/FloatWindowPermission

https://github.com/czy1121/settingscompat

看过大量文章之后找到这篇对悬浮权限的检测及跳转授权界面适配比较全面的文章,看过文章和源码之后简单的绘制了一张表格

api<19 api>=19 && api<23 api>=23
检测 默认拥有 小米华为魅族360需要检测其他默认拥有 通用的检测
请求 不用跳转 小米华为魅族360各自跳转其他不用跳转 通用的跳转

05. 权限检测

权限检测的总体方法

/**
 * 悬浮窗权限判断
 *
 * @param context 上下文
 * @return [ true, 有权限 ][ false, 无权限 ]
 */
private boolean checkPermission(Context context) {
  Boolean hasPermission = false;
  if (Build.VERSION.SDK_INT < 19) {
    hasPermission = true;
  } else if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 23) {
    if (RomUtils.checkIsMiuiRom() || RomUtils.checkIsMeizuRom() || RomUtils.checkIsHuaweiRom() || RomUtils.checkIs360Rom()) {// 特殊机型
      hasPermission = opPermissionCheck(context, 24);
    } else {// 其他机型
      hasPermission = true;
    }
  } else if (Build.VERSION.SDK_INT >= 23) {// 6.0 版本之后由于 google 增加了对悬浮窗权限的管理,所以方式就统一了
    hasPermission = highVersionPermissionCheck(context);
  }
  return hasPermission;
}

中间版本的特殊机型权限检测

/**
 * [19-23]之间版本通过[AppOpsManager]的权限判断
 *
 * @param context 上下文
 * @param op
 * @return [ true, 有权限 ][ false, 无权限 ]
 */
private boolean opPermissionCheck(Context context, int op) {
  try {
    AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
    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) {
    LogUtil.e(Log.getStackTraceString(e));
  }
  return false;
}

高版本的通用权限检测

/**
 * Android 6.0 版本及之后的权限判断
 *
 * @param context 上下文
 * @return [ true, 有权限 ][ false, 无权限 ]
 */
private boolean highVersionPermissionCheck(Context context) {
  if (RomUtils.checkIsMeizuRom()) {// 魅族6.0的系统单独适配
    return opPermissionCheck(context, 24);
  }
  try {
    Class clazz = Settings.class;
    Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class);
    return (Boolean) canDrawOverlays.invoke(null, context);
  } catch (Exception e) {
    LogUtil.e(Log.getStackTraceString(e));
  }
  return false;
}

源码中有提到魅族手机Android 6.0版本并不能使用通用的检测方式,需要使用低版本的方式。

06. 权限请求

总体的请求权限方法的伪代码

private void requestPermission() {
  if (Build.VERSION.SDK_INT < 19) {
    // 不用跳转
  } else if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 23) {
    if (RomUtils.checkIsMiuiRom() || RomUtils.checkIsMeizuRom() || RomUtils.checkIsHuaweiRom() || RomUtils.checkIs360Rom()) {
      // 分机型跳转
    } else {
      // 不用跳转
    }
  } else if (Build.VERSION.SDK_INT >= 23) {
    // 通用跳转
  }
}

总体的请求权限的方法

/**
 * 请求悬浮窗权限
 *
 * @param context 上下文
 */
private void requestPermission(Context context) {
  if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 23) {
    if (RomUtils.checkIsMiuiRom()) {
      miuiROMPermissionApply(context);
    } else if (RomUtils.checkIsMeizuRom()) {
      meizuROMPermissionApply(context);
    } else if (RomUtils.checkIsHuaweiRom()) {
      huaweiROMPermissionApply(context);
    } else if (RomUtils.checkIs360Rom()) {
      ROM360PermissionApply(context);
    }
  } else if (Build.VERSION.SDK_INT >= 23) {
    highVersionPermissionRequest(context);
  }
}

注意:中间版本权限的检测都是一样的,都是使用AppOpsManager类的方式并且参数op的值都是24,这里重点适配的是不同的手机跳转到不同的授权界面,而且小米手机各个版本的授权界面并不完全相同。

高版本的跳转通用授权界面

/**
 * Android 6.0 版本及之后的跳转权限申请界面
 *
 * @param context 上下文
 */
private void highVersionJump2PermissionActivity(Context context) {
  try {
    Class clazz = Settings.class;
    Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
    Intent intent = new Intent(field.get(null).toString());
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setData(Uri.parse("package:" + context.getPackageName()));
    context.startActivity(intent);
  } catch (Exception e) {
    Log.e(TAG, Log.getStackTraceString(e));
  }
}

这里同样需要注意魅族手机Android 6.0版本仍旧使用中间版本的方式。

07. 应用详情

在使用即刻APP的时候仔细观察了一下,发现即刻的视频悬浮同样需要使用权限,在没有授权限的时候会弹框提示用户去应用详情界面去设置权限,提示路径设置 > 应用 > 即刻,同时点击按钮直接跳转应用详情界面。这种方式虽然简单但其实并不是所有的机型都可以达到完美适配的情况,仍有部分机型在此界面是无法跳转到相关的手机位置,用户可能会一脸茫然。

/**
 * 跳转应用详情界面
 *
 * @param context
 */
private void jump2DetailActivity(Context context) {
  Intent intent = new Intent();
  intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  intent.setData(Uri.fromParts("package", context.getPackageName(), null));
  context.startActivity(intent);
}

这样便可以尝试简单的授权请求

/**
 * 请求悬浮窗权限
 *
 * @param context 上下文
 */
private void requestPermission(Context context) {
  if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 23) {
    jump2DetailActivity(context);
  } else if (Build.VERSION.SDK_INT >= 23) {
    highVersionPermissionRequest(context);
  }
}

08. 权限规避

  1. Android无需权限显示悬浮窗, 兼谈逆向分析app
  2. 突破小米悬浮窗权限控制--不需要权限的悬浮窗

其实这些文章中提到的规避方案都是在type类型的指定上:

  • WindowManager.LayoutParams.TYPE_TOAST
  • WindowManager.LayoutParams.TYPE_PHONE

09. 第三方库

强调一下找到的比较好的适配库

  • https://github.com/zhaozepeng/FloatWindowPermission
  • https://github.com/czy1121/settingscompat

10. 有趣的事

16年国庆买个了魅蓝 3s手机5.x的系统,用了一段时间后在某次写Demo中发现Toast吐不出来,不明原因,然而并没有在意。年底在自如上租了房子,电子门锁需要从手机上获取动态密码,照样是Toast吐不出来,尴尬了好几天,结果在一次倒腾中发现了手机管家中的权限管理,通知管理-悬浮窗。好些国产手机都有手机管家之类的系统软件,有相关权限管理,魅蓝 3s手机5.x的系统中的手机管理 > 权限管理 > 通知管理 > 悬浮窗就可以管理一个应用的悬浮窗权限,就在昨天我升级手机系统到Flyme 6.0.2.0A并且是Android 5.1结果相同路径的悬浮窗就变成了悬浮通知并能管理悬浮窗权限,另外应用详情中的悬浮窗变成了桌面悬浮窗真正管理悬浮窗权限。

你可能感兴趣的:(Android 悬浮窗及权限)