You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

开发需完成任务才可关闭的安卓闹钟APP的技术方案咨询

解决安卓任务型闹钟APP的核心问题:防关闭+权限适配

嘿,我刚好处理过类似的安卓闹钟APP需求,给你几个经过实战验证的方案,完美解决你遇到的两个核心问题——任务管理器杀Activity后闹钟停止,以及Overlay权限在小米等设备的适配痛点:


核心思路:分离铃声播放与任务交互逻辑

之前的问题本质是把铃声播放和任务UI绑定在了同一个Activity,一旦Activity被任务管理器销毁,铃声也跟着停止。正确的做法是把铃声播放放在优先级更高的前台服务中,任务Activity只负责展示交互,两者独立运行。


方案1:前台服务+锁屏任务Activity(最稳定通用)

这个方案能保证即使任务Activity被杀死,铃声依然持续播放,同时强制用户在锁屏状态下完成任务,无法绕过。

步骤1:实现前台服务播放铃声

前台服务是安卓系统优先级最高的服务类型,必须显示通知,几乎不会被系统或任务管理器杀死,适合用来承载铃声播放逻辑:

class AlarmRingtoneService : Service() {
    private var mediaPlayer: MediaPlayer? = null
    private var taskActivityIsAlive = false

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 1. 启动前台通知(安卓强制要求)
        val notification = NotificationCompat.Builder(this, "ALARM_CHANNEL")
            .setContentTitle("起床闹钟")
            .setContentText("完成数学题才能关闭")
            .setSmallIcon(R.drawable.ic_alarm)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .build()
        startForeground(1001, notification)

        // 2. 循环播放闹钟铃声
        mediaPlayer = MediaPlayer.create(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
        mediaPlayer?.isLooping = true
        mediaPlayer?.start()

        // 3. 启动任务Activity(锁屏可见)
        launchTaskActivity()

        // 服务被意外杀死后自动重启
        return START_STICKY
    }

    // 启动/重启任务Activity(防止被任务管理器杀死后无交互入口)
    private fun launchTaskActivity() {
        val intent = Intent(this, TaskVerificationActivity::class.java)
        intent.addFlags(
            Intent.FLAG_ACTIVITY_NEW_TASK or
            Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
            Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
        )
        startActivity(intent)
        taskActivityIsAlive = true
    }

    // 供Activity调用:完成任务后停止铃声并销毁服务
    fun stopAlarm() {
        mediaPlayer?.stop()
        mediaPlayer?.release()
        stopForeground(STOP_FOREGROUND_REMOVE)
        stopSelf()
    }

    // 监听Activity是否存活,若被杀死则重启
    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        if (!taskActivityIsAlive) {
            launchTaskActivity()
        }
    }

    inner class AlarmBinder : Binder() {
        fun getService() = this@AlarmRingtoneService
    }

    override fun onBind(intent: Intent?) = AlarmBinder()
}

步骤2:实现锁屏可见的任务Activity

让Activity在锁屏状态下直接显示,同时禁止用户通过返回键或任务管理器轻易退出:

class TaskVerificationActivity : AppCompatActivity() {
    private lateinit var alarmService: AlarmRingtoneService
    private var isServiceBound = false

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            alarmService = (service as AlarmRingtoneService.AlarmBinder).getService()
            isServiceBound = true
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            isServiceBound = false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 设置锁屏显示、点亮屏幕、解锁键盘
        window.addFlags(
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
            WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
            WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
            WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
        )
        setContentView(R.layout.activity_task_verification)

        // 绑定服务,完成任务后通知服务停止铃声
        btnCompleteTask.setOnClickListener {
            if (isServiceBound) {
                alarmService.stopAlarm()
            }
            finish()
        }
    }

    override fun onStart() {
        super.onStart()
        Intent(this, AlarmRingtoneService::class.java).also {
            bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        if (isServiceBound) {
            unbindService(serviceConnection)
            isServiceBound = false
        }
    }

    // 禁用返回键
    override fun onBackPressed() {
        // 空实现,不让用户通过返回键退出
    }
}

方案2:Overlay权限适配(针对强制防退出需求)

如果需要完全禁止用户切换到其他APP,Overlay悬浮窗是必要的,但要针对不同品牌设备做适配:

1. 通用权限申请逻辑

// 检查Overlay权限
fun checkOverlayPermission(context: Context): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        Settings.canDrawOverlays(context)
    } else {
        true // 6.0以下无需权限
    }
}

// 引导用户开启权限
fun requestOverlayPermission(activity: Activity, requestCode: Int) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val intent = Intent(
            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:${activity.packageName}")
        )
        activity.startActivityForResult(intent, requestCode)
    }
}

2. 小米/华为等厂商适配

部分国产ROM会把Overlay权限藏在专属设置页面,需要单独跳转:

// 跳转到小米悬浮窗权限设置
fun goToXiaomiOverlaySetting(context: Context) {
    try {
        val intent = Intent("miui://securitycenter/permissions")
        intent.setClassName(
            "com.miui.securitycenter",
            "com.miui.permcenter.permissions.PermissionsEditorActivity"
        )
        intent.putExtra("extra_pkgname", context.packageName)
        context.startActivity(intent)
    } catch (e: Exception) {
        // 跳转失败则走通用权限页
        requestOverlayPermission(context as Activity, 1002)
    }
}

3. 高优先级悬浮窗实现

fun showTaskOverlay(context: Context) {
    val windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
    val overlayView = LayoutInflater.from(context).inflate(R.layout.layout_task_overlay, null)

    val layoutParams = WindowManager.LayoutParams(
        WindowManager.LayoutParams.MATCH_PARENT,
        WindowManager.LayoutParams.MATCH_PARENT,
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            WindowManager.LayoutParams.TYPE_PHONE
        },
        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
        WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
        WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
        PixelFormat.TRANSLUCENT
    )

    windowManager.addView(overlayView, layoutParams)
}

最终推荐方案

优先采用方案1(前台服务+锁屏Activity),它不需要依赖Overlay权限,兼容性最好,且能满足核心需求;如果必须强制用户无法切换APP,再叠加方案2的Overlay适配,针对不同品牌设备做权限引导。

内容的提问来源于stack exchange,提问作者vương Thong

火山引擎 最新活动