Android实战进阶 - 用户闲置超时自动退出登录功能详解

在很多金融、银行类app中,当用户长时间未操作时会提示用户即将退出登录状态,我们这里就是为了解决该种场景的

关联篇

实践方案采用了双定时设计 + 广播组件

  1. 第一个定时器是保护时间,当用户xx分钟不操作app就会唤醒第二个定时器
  2. 第二个定时器会执行唤醒、退出等操作

思考点

  • 定时器绑定组件的生命周期
  • MotionEvent.ACTION_UP 下开启定时器
  • Timber 是一个 Log 框架,不需要删除即可
  • 关于 ConstValue 下的各常量,可以直接直接整理为统一类,也可以直接声明,看自己习惯
  • 定时器触发的退出登录弹框自己写一个就好,若有机会以后会补一个

软考还有好多题没刷...

超时保护

kotlin 复制代码
package cn.com.xx

import android.content.Intent
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent

/**
 * 长时间未操作唤醒、退出保护
 * */
class LongTimeNoOperated(val lifecycleOwner: LifecycleOwner): CountDownTimer(MILLIS_IN_FUTURE, 1000), LifecycleObserver, Runnable {
    
    companion object {
    	//该处定义为10分钟保护期
        private const val MILLIS_IN_FUTURE = 10 * 60 * 1000L + 20 * 1000
    }
    
    private var timer: CountDownTimer? = null
    //提醒弹框
    private var appDialog: AppDialog? = null
    
    init {
        lifecycleOwner.lifecycle.addObserver(this)
    }
    
    fun dispatchTouchEvent(ev: MotionEvent?) {
    	//获取触摸动作,如果ACTION_UP,计时开始
        if (ev?.action == MotionEvent.ACTION_UP) { 
            startTime()
        } else { //否则其他动作计时取消
            cancel()
        }
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun startTime() {
    	//判断用户是否处于登录状态(结合自己项目的判别方法)
        if (LoginInfo.isLogin()) {
            cancel()
            start()
        }
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        cancel()
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
    	//防止内存泄露
        if (appDialog?.isShowing == true) {
            appDialog?.dismiss()
        }
        appDialog = null
        timer?.cancel()
        timer = null
        cancel()
//        lifecycleOwner.lifecycle.removeObserver(this)
    }
    
    override fun onTick(millisUntilFinished: Long) = Unit
    
    override fun onFinish() {
        Handler(Looper.getMainLooper()).post(this)
    }
    
    override fun run() {
    	//当用户长时间未操作时交互方面会出现提示弹框,其实多久之后会自行退出账户
        appDialog = AppDialog.Builder()
                .setTitle("提示")
                .setDelayMillis(200)
                .setMessage("您长时间未操作,10秒后将自动为您退出账户")
                .setNeutralButton("继续使用") {
                    Timber.e("LongTimeNoOperated--->继续使用")
                    timer?.cancel()
                    timer = null
                    startTime() // 重新计时
                }.create()
                
		//第二个定时弹框,主要用户用户可见的显性倒计时(当下为10秒倒计时)
        timer = object : CountDownTimer(10 * 1000, 1000) {
            
            override fun onTick(millisUntilFinished: Long) {
                Handler(Looper.getMainLooper()).post {
                    appDialog?.setMessage("您长时间未操作,${millisUntilFinished / 1000 + 1}秒后将自动为您退出账户")
                }
            }
            
            override fun onFinish() {
                Timber.e("超时 LongTimeNoOperated --> onFinish()")
                Handler(Looper.getMainLooper()).post {
                    if (appDialog?.isShowing == true) {
                        appDialog?.dismiss()
                    }
                    appDialog = null
                    timer?.cancel()
                    timer = null
                    //这里才是触发倒计时结束后执行的核心逻辑入口
                    AppContext.sendBroadcast(Intent(ConstValue.LONG_TIME_NO_OPERATED_LOGIN_RECEIVER))
                }
            }
        }
        timer?.start()
        appDialog?.show()
    }
    
}

登出逻辑

关于四大组件之一的广播,可静态注册,也可动态注册,但是从某一版本后好像基本都要求动态注册了!

故一般在 MainActivityonCreate 直接 LogoutBroadcastReceiver(this) 注册即可! (不再单独写代码记录了)

kotlin 复制代码
package cn.com.xx

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent


class LogoutBroadcastReceiver(val activity: AppCompatActivity) : BroadcastReceiver(), LifecycleObserver {

    init {
        activity.lifecycle.addObserver(this)
        val intentFilter = IntentFilter()
        //在项目中其实有很多场景都会执行退出操作,例如账户被顶,系统维护、自行退出等场景,所以可以自行过滤多种广播
        //其他几种场景,后续有机会一起总结,关注顶部关联篇即可
        intentFilter.addAction(ConstValue.RE_LOGIN_RECEIVIR)
        intentFilter.addAction(ConstValue.SYS_MAINTAIN)
        intentFilter.addAction(ConstValue.LONG_TIME_NO_OPERATED_LOGIN_RECEIVER)
        activity.registerReceiver(this, intentFilter)
    }

    override fun onReceive(context: Context, intent: Intent) {
    	//根据不同业务场景执行不同逻辑就可以,以下仅是伪代码,用于提供开发思路(如有误导,可自行删除不同action下的执行逻辑)
        when (intent.action) {
            ConstValue.SYS_MAINTAIN -> { // 系统维护
                Timber.e("系统维护的广播 LogoutBroadcastReceiver --> 用户被踢出去")
                activity.startActivity(Intent(activity, MaintainDlgActivity::class.java).apply {
                	//intent可以传外部发送广播时传入的值
                    putExtra("notice", intent.getStringExtra("notice"))
                    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
                })
            }
            ConstValue.RE_LOGIN_RECEIVIR -> {// 弹窗提示重新登录
                Timber.e("弹窗提示 LogoutBroadcastReceiver --> 用户被踢出去")
                activity.startActivity(Intent(activity, LogoutActivity::class.java).apply {
                	//intent可以传固定值
                    putExtra("reLogin", true)
                    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
                })
            }
            ConstValue.LONG_TIME_NO_OPERATED_LOGIN_RECEIVER -> {// 退出
                Timber.e("超时 LogoutBroadcastReceiver --> 用户被踢出去")
                //可以直接在当下执行逻辑,也可以跳转到新类后执行,具体看业务场景
                LoginInfo.setToken("")
                LoginBean.getInstance().isGoHome = true
                LoginBean.getInstance().isKickoff = true
                ARouter.getInstance().build(RouterPath.USER_LOGIN_ACT).navigation()
            }
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    private fun onDestroy() {
        activity.unregisterReceiver(this)
    }
}

绑定组件

定时器需要关联对应的组件才能生效,不然自身可监听不到用户 action

注意:所有 Activity 需要继承 BaseActivity 才能实现全局监听,未继承类则会出现倒计时保护失效的情况!

kotlin 复制代码
package cn.com.xx

import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Bundle
import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity


open class BaseActivity : AppCompatActivity() {

    private lateinit var longTimeNoOperated: LongTimeNoOperated

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        longTimeNoOperated = LongTimeNoOperated(this)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        longTimeNoOperated.dispatchTouchEvent(ev)
        return super.dispatchTouchEvent(ev)
    }
}
相关推荐
杉氧36 分钟前
状态管理变迁史:为什么我们放弃了 Redux 选择 Zustand?
android·前端·react native
三少爷的鞋4 小时前
别再靠 Code Review 守底线:我做了一个静态分析项目 RedLine
android
y = xⁿ12 小时前
DeepSeek Harness 学习日记:关于Agent接口,工具调用的底层实现
android·java·学习
2601_9620664914 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
用户693717500138420 小时前
曾经安卓开发人手一个的 EventBus,为什么现在没人爱用了?
android·android studio
九皇叔叔1 天前
MySQL ORDER BY 性能优化详解:Using filesort、联合索引、ASC/DESC 与覆盖索引
android·adb
OriginCoding1 天前
用 AI Agent 协作完成一个 Android TOTP 应用:从需求边界到 v1.0.0
android·ai编程
杉氧1 天前
React 灵魂:深入剖析 Hooks 与副作用(Side Effects)陷阱
android·前端·react native
又见情义1 天前
RK3568 + RTL8211F 网络唤醒(WOL)功能适配全记录
android·网络·驱动开发
用户0934077735141 天前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos