Android悬浮窗的坑

使用悬浮窗需要申请权限:


<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>




class MainActivity : AppCompatActivity() {

    

    val requestFloatWindowPermissionCode = 100



    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !Settings.canDrawOverlays(this)) {

            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)

            startActivityForResult(intent, requestFloatWindowPermissionCode)

        }

    }



    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == requestFloatWindowPermissionCode 

            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {

            if (Settings.canDrawOverlays(this)) {

                Log.i("ABC", "用户给予悬浮窗权限了")

            } else {

                Log.i("ABC", "用户没给悬浮窗权限")

            }

        }

    }

}



Android6.0的坑

===========================================================================

我有个解决问题的技巧就是写个小Demo,这样就没有其他不相关的代码,Demo里只写关于悬浮窗的代码,新建一个项目,语言选Kotlin,最小SDK为15,其它都默认,我用的是最新版本的AndroidStudio,什么Gradle什么鬼的都是更新到了最新的,项目创建好之后打开MainActivity,写代码,如下:


class MainActivity : AppCompatActivity() {



    override fun onTouchEvent(event: MotionEvent): Boolean {

        if (event.action == MotionEvent.ACTION_DOWN) {

            val button = Button(this).apply { text = "你好"; setOnClickListener {

                Toast.makeText(context, "我不好", Toast.LENGTH_SHORT).show()

            } }



            val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

            windowManager.addView(button, WindowManager.LayoutParams().apply {

                type = WindowManager.LayoutParams.TYPE_TOAST

                flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

                width = WindowManager.LayoutParams.WRAP_CONTENT

                height = WindowManager.LayoutParams.WRAP_CONTENT

                gravity = Gravity.START or Gravity.BOTTOM

                format = PixelFormat.TRANSLUCENT

            })

        }

        return super.onTouchEvent(event)

    }



}



对,你没看错,就是onCreate方法都不要,奇怪吧,我就是要代码最小化,不相关的代码都不要,Activity是可以不要onCreate方法的,长知识了吧。这里只关注Android6.0版本,所以版本权限啊什么的不用管,上面代码中把一个Button设置为悬浮窗的界面内容,Buttion的点击事件是弹出一个Toast,悬浮窗的位置是在左下角,代码特别整洁特别少吧!运行后是一个空白的界面,如下:

在这里插入图片描述

点击界面的任意位置,此时在左下角会弹出悬浮窗,如下:

在这里插入图片描述

此时点击“你好”是可以弹出Toast的。

按下返回键,我们发现悬浮窗也不见了。重新运行并点击屏幕,再次出现悬浮窗,此时按Home键,这样创建悬浮窗的Activity就没有销毁,此时发现悬浮窗还在,如下:

在这里插入图片描述

此时点击“你好”,发现不起作用,但是这个代码运行到Android7.0是完全没问题的,这就是神奇之处,在这里花了我好多时间,百度了好多文章也找不到答案,最后实在没办法又去对比了比了n多次重构前的代码,最后发现有一个地方不同,就是获取WindowManager的地方,如下:


val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager



这里是使用application来获取的系统服务,谁能想到啊!!万万也想不到啊,按我之前学习的理解,系统服务一般是单例的,在哪里获取都一样,没想到activity.getSystemService与application.getSystemService竟然是有区别的,一万匹吃草的马跑过,浪费我一天的时间啊,这Android真的是,无处不在的坑!!

使用了application来获取WindowManager之后,问题就解决了,就算结束activity,只要不结束app,悬浮窗都可以显示,而且应用后台运行后,悬浮窗的点击事件也是正常的。

最后试验了下各种上下文获取的WindowManager对象,如下:


println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")

println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")

println("baseContext: ${baseContext.getSystemService(Context.WINDOW_SERVICE)}")

println("application: ${application.getSystemService(Context.WINDOW_SERVICE)}")

println("applicationContext: ${applicationContext.getSystemService(Context.WINDOW_SERVICE)}")



经过经验发现,只要是不同的对象,获取的WindowManager就不是同一个对象,如果是同一个对象,则获取的就是同一个,比如在一个Activity里面多次调用getSystemService来获取WindowManager对象将是同一个,在不同的Activity里面获取的将是不同的对象。而application和applicationContext获取的是相同的,并且它们是全局的,所以不论在哪个Activity中调用application去获取都将得到同一个WindownManager对象。

Android7.0的坑

===========================================================================

因为我们公司只需要适配Android6.0和Android7.1.1,所以这里把Android7.1.1的悬浮窗实现也记录一下。

上面的代码跑到Android7.1.1中会报如下错误:


WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?



这个问题比较好解决,百度也会有相应的答案,从Android7.1.1(api25)开始,不能使用TOAST类型了,因为此类型是不需要申请权限的,太流氓了,未经用户同意就可以显示悬浮窗有点流氓,所以要改成使用PHONE类型,此类型需要悬浮窗权限的,官方说明在此,我们在代码中加入动态权限申请:


// 如果是7.1.1以上的系统(包含7.1.1),则需要判断是否有悬浮窗权限

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !Settings.canDrawOverlays(this)) {

    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)

    startActivity(intent)

}



这段代码会判断是否是7.0以上的系统,如果是就判断是否有悬浮窗权限,如果没有就打开系统设置中权限申请界面,如下图:

在这里插入图片描述

如上图,需要指定我们的应用为“可出现在其他应用上的应用”,也就是添加悬浮窗权限,但是这里并没有显示出我们的应用,这是因为我们的应用并没有在清单文件中声明悬浮窗权限,如下:


<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />



添加这个权限之后,系统的申请悬浮窗界面就会列出我们的应用了,如下:

在这里插入图片描述

如上图,可以看到我们的应用的悬浮窗权限是“不允许”,我们设置为“允许”,再次运行,发现还是报这个错误:


WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?



问题的原因是Android7.1.1及以上的系统不能使用TYPE_TOAST这个类型了,需要使用TYPE_PHONE,代码如下:


// 如果是7.1.1以上的系统(包含7.1.1),则需要使用TYPE_PHONE类型

type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) WindowManager.LayoutParams.TYPE_PHONE

       else WindowManager.LayoutParams.TYPE_TOAST



最后有个小提示,我发现我们公司的那个app运行到android7.1.1手机上并不会跳出申请悬浮窗权限的界面,这是因为我们的app设置的targetSdkVersion为21(Android5.0),而动态权限申请是从Android6.0开始的,所以为了兼容旧版本,只要targetSdkVersion小于6.0就不需要动态申请权限,只需要在清单文件中声明即可。不过有些手机可能定制过的,即使targetSdkVersion小于6.0也会弹出隐私权限的动态申请对话框。

Android8.0的坑

===========================================================================

type要使用WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,否则会报如下异常:


permission denied for window type 2002



type的赋值逻辑如下:


type = when {

    Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

    Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 -> WindowManager.LayoutParams.TYPE_PHONE

    else -> WindowManager.LayoutParams.TYPE_TOAST

}



Toast攻击

======================================================================

转载于:https://unit42.paloaltonetworks.com/unit42-android-toast-overlay-attack-cloak-and-dagger-with-no-permissions/https://www.trendmicro.com/en_us/research/17/k/toast-overlay-weaponized-install-android-malware-single-attack-chain.html

Palo Alto Networks 42部门的研究人员发现了Android覆盖系统中的一个严重漏洞,该漏洞可以通过使用“ Toast类型”覆盖来进行新的Android覆盖攻击。所有操作系统版本低于8.0的Android设备均受此漏洞影响,并且补丁程序作为2017年9月Android安全公告的一部分提供。Android 8.0刚刚发布,不受此漏洞的影响。因为Android 8.0是最新的,所以此漏洞会影响当前市场上几乎所有的Android设备(请参阅表1),并且用户应尽快应用更新。

覆盖攻击使攻击者可以利用受影响的设备上运行的其他窗口和应用程序。要发起此类攻击,恶意软件通常需要请求“在顶部绘制”权限。但是,此新发现的覆盖攻击不需要任何特定权限或条件即可生效。发起此攻击的恶意软件不需要拥有覆盖权限,也不需要从Google Play安装。借助这种新的覆盖攻击,恶意软件可以诱使用户启用Android Accessibility Service并授予设备管理员特权或执行其他危险操作。如果授予了这些特权,则可以在设备上发起许多强大的攻击,包括窃取凭据,以静默方式安装应用程序以及为赎金而锁定设备。

最后

总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。

在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

[外链图片转存中…(img-A5LYTAbm-1725734001597)]

[外链图片转存中…(img-7wvuFcbK-1725734001598)]

[外链图片转存中…(img-FdaBJIXg-1725734001599)]

还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值