现在越来越多的Android APP都有悬浮窗的功能,公司项目中最新的需求也需要加入悬浮窗的功能,这次的功能是指在应用内的悬浮窗(而不是系统级别的悬浮窗)。悬浮窗功能的时候,整体分了2个部分:
- 悬浮窗的权限申请问题
- 悬浮窗的实现问题
因为Android碎片化的问题,所以第一开始适配这边的权限问题的时候头皮发麻。在参考了一下文章后,总结了思路
https://blog.csdn.net/self_study/article/details/52859790
https://www.jianshu.com/p/167fd5f47d5c
我们知道window的type分为3类:
1. Application windows
应用窗口,比如Activity的窗口;
在FIRST_APPLICATION_WINDOW与
LAST_APPLICATION_WINDOW`之间,是常用的顶层应用程序窗口,须将token设置成Activity的token;
2. Sub-windows
子窗口,依赖父窗口,比如PopupWindow;
在FIRST_SUB_WINDOW
和LAST_SUB_WINDOW
之间,与顶层窗口相关联,需将token设置成它所附着宿主窗口的token;
3. System windows
系统窗口,比如状态栏、Toast、悬浮窗;
在FIRST_SYSTEM_WINDOW
和LAST_SYSTEM_WINDOW
之间,不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用。
悬浮窗其实是通过通过getSystemService(Context.WINDOW_SERVICE)
拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams。如果要做系统级别的悬浮窗(应用退出后,悬浮窗仍然显示在桌面上),获取context.getSystemService(Context.WINDOW_SERVICE)
,我们要做的是应用内的悬浮窗,所以直接获取Activit级别的WindowManager即可,activity.getSystemService(Context.WINDOW_SERVICE)
,layoutParams.type是设置悬浮窗的类型的。
WindowManager windowManager = (WindowManager) activity.getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
...(设置layoutParams其他的参数)
windowManager.addView(view,layoutParams);
添加悬浮窗时,是需要申请权限的。
通常我们会有两种做法,一是在开启悬浮窗之前,引导用户去开启权限。权限一般路径
设置--应用程序--应用--权限管理--在其他应用的上层显示
,个别手机路径略有不同。
二是绕过权限管理,直接开启权限
在addView之前会有一个PhoneWindowManager.checkAddPermission()
检查权限的操作
7.1源码中checkAddPermission()
里面中
...
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:
case TYPE_QS_DIALOG:
// 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 (android.Manifest.permission.SYSTEM_ALERT_WINDOW.equals(permission)) {
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.checkOpNoThrow(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:
try {
ApplicationInfo appInfo = mContext.getPackageManager()
.getApplicationInfo(attrs.packageName,
UserHandle.getUserId(callingUid));
// Don't crash legacy apps
if (appInfo.targetSdkVersion < Build.VERSION_CODES.M) {
return WindowManagerGlobal.ADD_OKAY;
}
} catch (PackageManager.NameNotFoundException e) {
/* ignore */
}
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;
}
...
从以下代码可知道 TYPE_PHONE
和TYPE_SYSTEM_ALERT
是有权限的
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;
android.Manifest.permission.SYSTEM_ALERT_WINDOW
而这个权限是需要我们去动态申请和手动开启的(这个权限是默认关闭的)。
既然 TYPE_PHONE
是需要判断权限,那 TYPE_TOAST
是不需要判断权限的,使用 TYPE_TOAST
不就行了?
使用 TYPE_TOAST
4.4之前的版本接收不到触摸事件和点击事件了 ,所以我们只能放弃了。
Android 4.4以下是不需要申请权限,可以直接添加。4.4以上是需要申请权限的,4.4-5.1检查权限是使用AppOpsManager
中的checkOp()
方法的,6.0的权限申请是单独管理的,和4.4-5.1的方法不一样。所以我们申请权限是需要分开申请。
private boolean checkFloatPermission(Context context) {
if (Build.VERSION.SDK_INT >= 23) {//6.0以上
boolean result = false;
try {
Class clazz = Settings.class;
Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class);
result = (boolean) canDrawOverlays.invoke(null, context);
Log.e(TAG, "checkFloatPermission:-->" + result);
} catch (Exception e) {
e.printStackTrace();
}
return result;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4-5.1
return getAppOps(context);
} else {//4.4以下
return true;
}
}
4.4-5.1版本的权限判断是通过反射来获取的
private static boolean getAppOps(Context context) {
try {
Object object = context.getSystemService(context.APP_OPS_SERVICE);
if (object == null) {
return false;
}
Class localClass = object.getClass();
Class[] arrayOfClass = new Class[3];
arrayOfClass[0] = Integer.TYPE;
arrayOfClass[1] = Integer.TYPE;
arrayOfClass[2] = String.class;
Method method = localClass.getMethod("checkOp", arrayOfClass);
if (method == null) {
return false;
}
Object[] arrayOfObject1 = new Object[3];
arrayOfObject1[0] = Integer.valueOf(24);
arrayOfObject1[1] = Integer.valueOf(Binder.getCallingUid());
arrayOfObject1[2] = context.getPackageName();
int m = ((Integer) method.invoke(object, arrayOfObject1)).intValue();
return m == AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
Log.e(TAG, "permissions judge: -->" + e.toString());
}
return false;
}
如果权限没有开启,需要我们提示并引导用户去开始权限。
if (Build.VERSION.SDK_INT >= 23) {//6.0以上
try{
Intent intent=new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivityForResult(intent, CHECKPRESSMISSON_CODE);
}catch (Exception e)
{
e.printStackTrace();
}
}
6.0的权限是单独的,跳转到开启悬浮窗页面后,在onActivityResult()
中对不同情况做不同的逻辑处理。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CHECKPRESSMISSON_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
Log.e(TAG, "已经打开悬浮窗权限");
//进行下一步逻辑处理
} else {
//提示用户需要开启
}
}
}
}
4.4-5.1的手机就提示并引导用户去设置页面打开悬浮窗的权限
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
intent.setData(Uri.fromParts("package", getPackageName(), null));
对Android应用中悬浮窗的权限问题简单的总结了下,而设置悬浮窗的问题可以去看下一篇
https://mp.csdn.net/mdeditor/80468278