返回

Android 构建用户确认的防跳过覆盖层开发指南

Android

Android 开发:构建用户确认的防跳过覆盖层

在 Android 系统中实现用户同意且无法跳过的覆盖层是一个富有挑战的任务。此目标主要是要实现在应用需要的时候可以随时触发覆盖层的展示, 用户确认应用实施此类动作。 这是一种通过创建自我控制覆盖层,来阻止用户无意识地使用某些应用和网站的手段。尽管 Android 系统持续增强了用户体验和安全性, 但它同样带来了一些限制,如何平衡好系统对用户的保护机制以及应用提供的限制方式,成为了该需求的技术核心。

面临的挑战

用户确认并提供应用实施一些强制控制策略的支持后,这些强制控制行为却会因为系统出于保护用户而做出拒绝。

开发者通常会遇到由于权限限制,或新版本 Android 策略变化而无法正常实现覆盖层的情况。Android 近年来为了更好地管理系统资源与后台运行,不断对后台进程管理机制 以及权限要求 进行迭代, 同时也加入更多的新策略对后台以及覆盖层应用进行控制,这些都成为无法正常实现的技术难题 。 怎样在这种策略的夹缝中寻找解决方法。 系统希望尽可能地让应用保持自生自灭,以维护用户手中的电源健康、内存等资源的平衡以及保护用户的安全与隐私,而这些保护政策无意对这些希望用户同意而实行的保护措施拒之门外。

SYSTEM_ALERT_WINDOW 权限也成为关键点, 系统要求应用进行声明和提示, 以保护用户免受应用过度滥用弹窗等行为的保护。而应用的生命周期同样被后台控制的进程不断调优,使得实现长时保护与运行十分困难。 怎样的架构或者机制设计能突破这个瓶颈。

解决方法探讨

在符合规范条件下,实现一个用户认可的、防跳过的、始终存在的覆盖层并非完全不可能。可以结合前台服务悬浮窗 以及辅助功能服务 的方式来实施。

一、利用前台服务结合悬浮窗

创建前台服务,提升应用的优先级,尽可能延长应用生命周期。并通过 SYSTEM_ALERT_WINDOW 权限,在最上层显示悬浮窗作为覆盖层。
必须在 manifest 中进行申明:

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

并在应用的启动阶段提示用户进行SYSTEM_ALERT_WINDOW 的权限请求:

if (!Settings.canDrawOverlays(this)) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
    startActivityForResult(intent, REQUEST_CODE)
}

利用WindowManager,在用户已经允许权限的情况下,添加一个View作为覆盖层。
代码示例:

  1. 创建并启动一个前台服务,防止应用被轻易杀死。

    class OverlayService : Service() {
    
        private lateinit var windowManager: WindowManager
        private lateinit var overlayView: View
    
        override fun onBind(intent: Intent): IBinder? {
            return null
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            //创建通知,作为前台服务的入口.
            val notification = createNotification() // 具体实现细节请参见开发者文档
            startForeground(1, notification)
            showOverlay()
            return START_STICKY
        }
    
        private fun showOverlay() {
            windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
            overlayView = LayoutInflater.from(this).inflate(R.layout.overlay, null)
    
            val params = WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT
            )
    
            windowManager.addView(overlayView, params)
        }
    
        override fun onDestroy() {
            super.onDestroy()
            windowManager.removeView(overlayView)
        }
    
         private fun createNotification(): Notification {
             val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            //  高版本安卓需要NotificationChannel, 请参考开发者文档适配高版本机型。
             return NotificationCompat.Builder(this, "channel_id")
                .setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle("防沉迷")
                .setContentText("覆盖层正在运行")
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setOngoing(true) // 设置通知无法被用户滑动清除
                .build()
        }
    }
    
  2. 编写布局文件,实现想要的覆盖效果。

    • 覆盖层中通过实现触摸事件阻止任何操作传递给下层应用,避免穿透到下面的视图造成可能的错误响应或者绕过。
    • 设置FLAG_NOT_FOCUSABLE 以及FLAG_NOT_TOUCH_MODAL 以使下层视图仍能进行输入等操作,覆盖层本身不成为焦点,使用透明或者有部分穿透。
  3. 启动服务:

    val serviceIntent = Intent(this, OverlayService::class.java)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
       startForegroundService(serviceIntent)
     }else{
         startService(serviceIntent)
     }
    
    

注意事项:

  • 不同 Android 版本可能存在兼容性问题,TYPE_APPLICATION_OVERLAY 在部分安卓版本中已经被限制和不推荐,请针对用户所处的机型进行不同的代码适配处理。
  • 即使用户授权SYSTEM_ALERT_WINDOW, 也必须尽可能节制使用,减少用户不便。
  • 必须清晰明确告知用户应用的各项权限要求和理由。
  • 保持服务在低电量,低内存中被意外关闭的可能性,在意外关闭中也需要有措施尽可能恢复状态或以应用可支持的方式与用户继续互动。

二、使用辅助功能服务

Accessibility Service 主要用于增强用户与应用的互动方式,辅助用户浏览屏幕上可用的项目。但我们也可使用该服务,获得系统全局事件来操作应用的显示,实现更加稳定的控制方案。

实现步骤:

  1. 声明并创建辅助功能服务。

    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
        <intent-filter>
            <action android:name="android.accessibilityservice.AccessibilityService" />
        </intent-filter>
        <meta-data android:name="android.accessibilityservice"
            android:resource="@xml/accessibility_service_config" />
    </service>
    
  2. 创建辅助功能服务的配置xml文件。

    <accessibility-service
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
        android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="default"
        android:canRetrieveWindowContent="true"
        android:description="@string/accessibility_service_description"
        android:notificationTimeout="100"
        android:packageNames="com.example.targetapp"
    />
    

    设置packageNames以限制服务的作用域到特定的应用。设置监视事件为窗口内容变化。

  3. 实现辅助功能服务逻辑。

    class MyAccessibilityService : AccessibilityService() {
    
        override fun onAccessibilityEvent(event: AccessibilityEvent?) {
    
         when (event?.eventType) {
                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
                    //可以判断并控制页面的状态.
                }
        }
    
        override fun onInterrupt() {
        }
    
        private fun handleWindowsChangedEvent(rootInActiveWindow: AccessibilityNodeInfo?) {
    
        }
    }
    

优势:

  • 相比 SYSTEM_ALERT_WINDOW, AccessibilityService 的能力更广泛, 可以用以访问与处理更多事件。
  • 可以在特定应用启动或指定时机创建自己的显示组件来影响界面状态。

注意事项:

  • 合理配置辅助功能服务,避免过度消耗系统资源。
  • 辅助功能服务涉及到操作全局应用,请仅用于被允许以及正当的目的。

总结

无论是采用前台服务结合悬浮窗还是使用辅助功能服务,都需要确保获得用户授权并合理告知使用目的,保持最大克制来平衡体验。由于不同Android 系统版本限制,要尽可能详细的测试,防止不合理的适配导致错误。不断与系统迭代一起更新技术栈。同时,系统为用户设计的一系列权限策略也可能进一步修改和变化,开发者只有与用户良好沟通,才能让这些努力保持长时间有效。