我正在为父母开发MDM应用程序来控制孩子的设备,如果执行了禁止的操作,它使用权限SYSTEM_ALERT_WINDOW
在设备上显示警告。 在装有 SDK 23+ (Android 6.0) 的设备上,应用在安装过程中会使用此方法检查权限:
Settings.canDrawOverlays(getApplicationContext())
如果此方法返回false
则应用程序将打开系统对话框,用户可以在其中授予权限:
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
但在装有 SDK 26 (Android 8.0) 的设备上,当用户成功授予权限并通过按返回按钮返回应用时,方法canDrawOverlays()
仍会返回false
,直到用户不关闭应用并重新启动它,或者只是在最近的应用对话框中选择它。我在AndroidStudio中使用Android 8在最新版本的虚拟设备上对其进行了测试,因为我没有真正的设备。
我做了一些研究,并另外检查了AppOpsManager的权限:
AppOpsManager appOpsMgr = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
int mode = appOpsMgr.checkOpNoThrow("android:system_alert_window", android.os.Process.myUid(), getPackageName());
Log.d(TAG, "android:system_alert_window: mode=" + mode);
所以:
- 当应用程序没有此权限时,模式为"2" (MODE_ERRORED)(
canDrawOverlays()
返回false
) 当用户 - 授予权限并返回给应用程序,模式为"1" (MODE_IGNORED)(
canDrawOverlays()
返回false
) - 如果您现在重新启动应用程序,则模式为"0"(MODE_ALLOWED)(
canDrawOverlays()
返回true
)
拜托,谁能向我解释这种行为?是否可以依赖操作"android:system_alert_window"
mode == 1
并假定用户已授予权限?
我遇到了同样的问题。我使用了一种尝试添加不可见覆盖的解决方法。如果引发异常,则不会授予权限。这可能不是最好的解决方案,但它有效。 我无法告诉您有关AppOps解决方案的任何信息,但它看起来很可靠。
编辑 2020 年 10 月:如评论中所述,抛出SecurityException
时WindowManager
内可能存在内存泄漏,导致解决方法视图无法删除(即使显式调用removeView
)。对于正常使用,这应该不是什么大问题,但避免在同一应用程序会话中运行太多检查(如果不进行测试,我认为低于一百的任何内容都应该没问题)。
/**
* Workaround for Android O
*/
public static boolean canDrawOverlays(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true;
else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
return Settings.canDrawOverlays(context);
} else {
if (Settings.canDrawOverlays(context)) return true;
try {
WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (mgr == null) return false; //getSystemService might return null
View viewToAdd = new View(context);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
viewToAdd.setLayoutParams(params);
mgr.addView(viewToAdd, params);
mgr.removeView(viewToAdd);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
我也发现了 checkOp 的问题。就我而言,我有一个流程,它允许仅在未设置权限时重定向到设置。并且仅在重定向到设置时设置 AppOps。
假设 AppOps 回调仅在更改某些内容且只有一个可以更改的开关时调用。这意味着,如果调用回调,用户必须授予权限。
if (VERSION.SDK_INT >= VERSION_CODES.O &&
(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW.equals(op) &&
packageName.equals(mContext.getPackageName()))) {
// proceed to back to your app
}
应用程序恢复后,检查canDrawOverlays()
开始对我有用。我确定重新启动应用程序并检查是否通过标准方式授予权限。
这绝对不是一个完美的解决方案,但它应该有效,直到我们从谷歌那里了解更多。
编辑:我问谷歌:https://issuetracker.google.com/issues/66072795
编辑2:谷歌修复了这个问题。但似乎Android O版本仍然会受到影响。
这是我的多合一解决方案,这是其他解决方案的组合,但在大多数情况下都很好
根据 Android 文档使用标准检查进行首次检查
第二个检查是使用 AppOpsManager
如果所有其他方法都失败,第三个也是最后一个检查是尝试显示覆盖层,如果失败,它肯定会工作;)
static boolean canDrawOverlays(Context context) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M && Settings.canDrawOverlays(context)) return true;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {//USING APP OPS MANAGER
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
if (manager != null) {
try {
int result = manager.checkOp(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, Binder.getCallingUid(), context.getPackageName());
return result == AppOpsManager.MODE_ALLOWED;
} catch (Exception ignore) {
}
}
}
try {//IF This Fails, we definitely can't do it
WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (mgr == null) return false; //getSystemService might return null
View viewToAdd = new View(context);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
viewToAdd.setLayoutParams(params);
mgr.addView(viewToAdd, params);
mgr.removeView(viewToAdd);
return true;
} catch (Exception ignore) {
}
return false;
}
实际上在Android 8.0上,它会返回true
但仅当您等待5到15秒并使用Settings.canDrawOverlays(context)
方法再次查询权限时。
因此,您需要做的是向用户显示一个ProgressDialog
,其中包含一条解释问题的消息,并运行CountDownTimer
以使用onTick
方法中的Settings.canDrawOverlays(context)
检查覆盖权限。
下面是示例代码:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && !Settings.canDrawOverlays(getApplicationContext())) {
//TODO show non cancellable dialog
new CountDownTimer(15000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
if (Settings.canDrawOverlays(getApplicationContext())) {
this.cancel(); // cancel the timer
// Overlay permission granted
//TODO dismiss dialog and continue
}
}
@Override
public void onFinish() {
//TODO dismiss dialog
if (Settings.canDrawOverlays(getApplicationContext())) {
//TODO Overlay permission granted
} else {
//TODO user may have denied it.
}
}
}.start();
}
}
就我而言,我的目标是 Oreo 为了防止您的活动在后台被破坏,在 在 需要注意的是,从当前实现(Android O)开始,系统不会在回调之前删除重复的注册侦听器。 注册 上述方法的替代方案是在调用onCreate
内部if (savedInstanceState != null) {
canDraw = Settings.canDrawOverlays(context);
}
onDestroy
中,如果onOpChangedListener
不为 null,则应调用stopWatchingMode
,类似于上面的onActivityResult
。startWatchingMode(ops, packageName, listener)
将导致侦听器被调用以进行匹配操作或匹配包名称,如果它们都匹配,将被调用 2 次,因此包名称在上面设置为 null 以避免重复调用。 此外,多次注册侦听器,而不由stopWatchingMode
取消注册,将导致侦听器被多次调用 - 这也适用于活动销毁-创建生命周期。Settings.canDrawOverlays(context)
之前设置大约 1 秒的延迟,但延迟的值取决于设备并且可能不可靠。(参考资料:https://issuetracker.google.com/issues/62047810)
如果您检查几次,它将正常工作...
我的解决方案是:
if (!Settings.canDrawOverlays(this)) {
switchMaterialDraw.setChecked(false);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (Settings.canDrawOverlays(HomeActivity.this)){
switchMaterialDraw.setChecked(true);
}
}
}, 500);
switchMaterialDraw.setChecked(false);
} else {
switchMaterialDraw.setChecked(true);
}
如您所知,有一个已知的错误:Settings.canDrawOverlays(context)
总是在Android 8和8.1的某些设备上返回false
。 目前,最好的答案是通过快速测试@Ch4t4r,但它仍然存在缺陷。
-
它假设如果当前的Android版本是8.1 +,我们可以依靠该方法
Settings.canDrawOverlays(context)
但实际上,我们不能,根据多个评论。在某些设备上的 8.1 上,即使刚刚收到权限,我们仍然可以获得false
。if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) return Settings.canDrawOverlays(context); //Wrong
-
它假设如果我们尝试在没有覆盖权限的情况下向窗口添加视图,系统将引发异常,但在某些设备上并非如此(许多中国制造商,例如小米),因此我们不能完全依赖
try-catch
-
最后一件事,当我们向窗口添加一个视图并在下一行代码中删除它时 - 不会添加视图。实际添加它需要一些时间,因此这些行不会有太大帮助:
mgr.addView(viewToAdd, params); mgr.removeView(viewToAdd); // the view is removed even before it was added
这导致了一个问题,如果我们尝试检查视图是否实际附加到窗口,我们将立即获得false
。
好的,我们将如何解决这个问题?
因此,我使用覆盖权限快速测试方法更新了此解决方法,并添加了一点延迟,以便为视图提供一些时间以附加到窗口。 因此,我们将使用带有回调方法的侦听器,该方法将在测试完成时通知我们:
-
创建一个简单的回调接口:
interface OverlayCheckedListener { void onOverlayPermissionChecked(boolean isOverlayPermissionOK); }
-
当用户应该启用权限时调用它,我们需要检查他是否直接启用了它(例如
onActivityResult()
):private void checkOverlayAndInitUi() { showProgressBar(); canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() { @Override public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) { hideProgressBar(); initUi(isOverlayPermissionOK); } }); }
-
方法本身。神奇的数字 500 - 在询问视图是否附加到窗口之前,我们延迟了多少毫秒。
public static void canDrawOverlaysAfterUserWasAskedToEnableIt(Context context, final OverlayCheckedListener listener) { if(context == null || listener == null) return; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { listener.onOverlayPermissionChecked(true); return; } else { if (Settings.canDrawOverlays(context)) { listener.onOverlayPermissionChecked(true); return; } try { final WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (mgr == null) { listener.onOverlayPermissionChecked(false); return; //getSystemService might return null } final View viewToAdd = new View(context); WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); viewToAdd.setLayoutParams(params); mgr.addView(viewToAdd, params); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { if (listener != null && viewToAdd != null && mgr != null) { listener.onOverlayPermissionChecked(viewToAdd.isAttachedToWindow()); mgr.removeView(viewToAdd); } } }, 500); } catch (Exception e) { listener.onOverlayPermissionChecked(false); } } }
注意:这不是一个完美的解决方案,如果您想出更好的解决方案,请告诉我。此外,由于 postDelay 操作,我遇到了一些奇怪的行为,但似乎原因出在我的代码中而不是在方法中。
希望这对某人有所帮助!
基于@Vikas Patidar 答案的 Kotlin 协程版本
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// see https://stackoverflow.com/questions/46173460/why-in-android-8-method-settings-candrawoverlays-returns-false-when-user-has
if (requestCode == <your_code> && !PermsChecker.hasOverlay(applicationContext)) {
lifecycleScope.launch {
for (i in 1..150) {
delay(100)
if (PermsChecker.hasOverlay(applicationContext)) {
// todo update smth
break
}
}
}
}
}