1. The Android listening page has no operation and returns periodically

If there is a new requirement for an existing project, you need to listen for any operation on the page. If no operation is performed within a certain period of time, you need to return to the specified page. Some customized systems are not safe if they stay on the project debug page for a long time, so they need to go back to the home page, as well as the TV box can jump to ads/screensavers in the perception of no operation.

Since it was an existing project, we wanted to complete our features with as little code intrusion as possible.

2. Functional analysis

  1. The first thing you need is a timer. The general idea is to set the timer, if there is an operation was cancelled, another new timer, here we use more a little bit more simple method, using a timer, when perceived is updated operation time, if the current time and operation time in the timer value reaches a certain time to trigger our return to the business.

  2. How to notify: broadcast, EventBus/RxBus, or custom callback, if you can send the event back

  3. How to get touch event updates our app is based on the Activity, so just listen for ACTION_UP in the Activity, rewrite the dispatchTouchEvent, insert our time update code

Three, implementation,

Let’s write a simple code:

Notification is implemented by broadcast

//ActivityMonitor

class ActivityMonitor {

    private var recordTime = System.currentTimeMillis()// Record the operation time
    private var disposable: Disposable? = null/ / timer
    private var context: Context? = null

    companion object {
        @JvmStatic
        fun get(a): ActivityMonitor {
            return Holder.holder
        }
    }

    object Holder {
        @SuppressLint("StaticFieldLeak")
        val holder = ActivityMonitor()
    }

    fun attach(context: Context) {
        Log.d("zhou"."attach $context")
        this@ActivityMonitor.context = context
        Log.d("zhou"."ActivityMonitor >> $context")}// Create a timer
    private fun createDisposable(a): Disposable {
        Log.d("zhou"."createDisposable")
        return Observable.interval(2, TimeUnit.SECONDS)
                .subscribe {
                    Log.d("zhou"."time === didi......")
                    val time = (System.currentTimeMillis() - recordTime) / 1000
                    if (time > 5) {
                        Log.d("zhou"."timeout...")
                        this@ActivityMonitor.context!! .sendBroadcast(Intent(GV.MONITOR_TIMEOUT)) disposable? .apply {if(! isDisposed) { dispose() } disposable =null}}else {
                        this@ActivityMonitor.context!! .sendBroadcast(Intent().apply { action = GV.MONITOR_TIME_COUNT putExtra("msg"."update >> $it current diff = $time")})}}}fun update(a) {// Update time
        Log.d("zhou"."update operate time.")
        recordTime = System.currentTimeMillis()
        if (disposable == null) {
            disposable = createDisposable()
        }
        this@ActivityMonitor.context? .sendBroadcast(Intent().apply { action = GV.MONITOR_TIME_COUNT putExtra("msg"."on touch")})}fun cancel(a) {/ / cancel
        Log.e("zhou"."cancel") disposable? .also {if(! it.isDisposed) { it.dispose() } disposable =null}}}Copy the code

A quick explanation:

  1. attachSet it in Application just to get the context and broadcast it
  2. updateUpdate time
  3. cancelCancel the timing, such as monitoring the page before the end of the timing, we should cancel this time

Here is an example of an Activity call:

class UndoTestActivity : BaseActivity() {

	/ / to omit...
	override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
		registerReceiver(receiver, IntentFilter().apply {
            addAction(GV.MONITOR_TIMEOUT)
            addAction(GV.MONITOR_TIME_COUNT)
        })
        ActivityMonitor.get().update()
	}

	override fun onDestroy(a) {
        super.onDestroy()
        unregisterReceiver(receiver)
        ActivityMonitor.get().cancel()
    }

    override fun dispatchTouchEvent(ev: MotionEvent?).: Boolean {
        if(ev? .action==MotionEvent.ACTION_UP){ ActivityMonitor.get().update()
        }
        return super.dispatchTouchEvent(ev)
    }

	// This is a radio notification, so register it
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context? , intent:Intent?).{ intent? .also {when (it.action) {
                    GV.MONITOR_TIME_COUNT -> {
                        val t = "${it.getStringExtra("msg")}\n"
                        text.append(t)
                    }
                    GV.MONITOR_TIMEOUT -> {
                        Log.i("zhou"."UndoTestActivity received msg,finish")
                        ToastUtils.show(this@UndoTestActivity."timeout,finish!!")
                        finish()
                    }
                }
            }
        }
    }
}

Copy the code

Most projects have a BaseActivity, which can be introduced in Base

Here is the test effect GIF

Four, thinking

This works well and the basic functions are implemented, but there seems to be too much code to insert, and some pages we allow to stay, that also needs to add a whitelist function.

Code optimization

1. Add a whitelist

The whitelist is passed directly during initialization

class ActivityMonitor{
    fun attach(context: Context, list: ArrayList<Any>) {
        
        this@ActivityMonitor.context = context
        if (list.isNotEmpty()) {
            uncheckList.addAll(list)
        }
    }
}

class App:Application() {@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        ActivityMonitor.get().attach(base, arrayListOf(MainActivity::class))}}Copy the code
2. Reduce caller code

We should hide our business logic and provide more concise calls for users to use

The Activity dispatchToucEvent is preprocessed with a window, which is a PhoneWindow

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
Copy the code

Callback = window.callback = window.callback = window.callback = window.callback

Class MonitorCalback(Val default: window.callback) : window.callback {// omit other overwrite functions... override fun dispatchTouchEvent(event: MotionEvent?) : Boolean {if(MotionEvent.ACTION_UP == event? .action) { ActivityMonitor.get().update() }returnDefault. DispatchTouchEvent (event)}} / / control class, increase the white list class ActivityMonitor {/ /... Fun Inject (activity: Activity, window: window) {// If it is a whitelist member, cancel it; otherwise, proxyif (uncheckList.contains(activity::class)) {
            cancel()
        } else{window.callback = MonitorCalback(window.callback) update()}} Fun onDestroy(activity: activity){if(uncheckList.contains(activity::class)){
            update()
        }else// Inject and cancel class UndoTestActivity on the page:BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ActivityMonitor.get().inject(this,window)
    }

    override fun onDestroy() {
        super.onDestroy()
        ActivityMonitor.get().onDestroy(this)
    }
}

Copy the code

Of course, there is a better way to implement proxy, using proxy mode need to write a bit of code, do not pay attention to the possibility of error, with dynamic proxy mode directly insert our own time to update the code, the so-called “section”, as follows:

To create a new proxy class, Java dynamic proxies can only proxy interfaces, if you want to proxy classes can use Cglib, not expanded here.

class WindowCallbackInvocation(val callback: Any) : InvocationHandler { override fun invoke(proxy: Any? , method: Method? , args: Array<out Any>?) : Any? {if ("dispatchTouchEvent"== method? .name) { Log.i("zhou"."WindowCallbackInvocation") val event: MotionEvent = args? .get(0) as MotionEventif (MotionEvent.ACTION_UP == event.action) {
                ActivityMonitor.get().update()
            }
        }
        returnmethod? .invoke(callback, *(args ? : arrayOfNulls<Any>(0))) } }Copy the code

Inject inject {SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC/SRC

fun inject(activity: Activity, window: Window) {
    if (uncheckList.contains(activity::class)) {
        cancel()
    } else {
        // Proxy mode
		//window.callback = MonitorCalback(window.callback)

        // Dynamic proxy
        val callback = window.callback
        val handler = WindowCallbackInvocation(callback)
        val proxy: Window.Callback = Proxy.newProxyInstance(Window.Callback::class.java.classLoader.arrayOf(Window.Callback::class.java), handler) as Window.Callback
        window.callback = proxy
        
        update()
    }
}
Copy the code

At this point, our monitoring class is complete.

3. Whether there are unconsidered points

This monitoring class is only a proxy for the PhoneWindow in the Activity. What are the potential dangers here?

Yes, if the Activity uses a popover such as Dialog/AlertDialog or PopupWindow, then we can’t intercept the event updates, Because they are a new PhoneWindow (Popup uses PopupDecorView so window.callback is not used) and we are not intercepting them, the user is doing something when the Popup pops up but we are not updating the timer. Therefore, we need to modify inject method to support Dialog (not limited to Activity), while PopupWindow is not applicable and can only cancel the timer when calling show. In dismiss, restart the timer. The code is modified as follows:

class ActivityMonitor{
    // omit others
    fun inject(clz: Class<Any>, window: Window) {
        if (uncheckList.contains(clz)) {
            cancel()
        } else {
            // Proxy mode
            //window.callback = MonitorCalback(window.callback)

            // Dynamic proxy
            val callback = window.callback
            val handler = WindowCallbackInvocation(callback)
            val proxy: Window.Callback = Proxy.newProxyInstance(Window.Callback::class.java.classLoader.arrayOf(Window.Callback::class.java), handler) as Window.Callback
            window.callback = proxy
            update()
        }
    }
    
    fun onDestroy(clz: Class<Any>) {
        if (uncheckList.contains(clz)) {
            update()
        } else {
            cancel()
        }
    }
}
Copy the code

This is not the best solution, and if the application itself had lots of popovers, I would have died on the spot. So, there’s room for improvement in this code.

Five, the summary

Now let’s get the idea, there is no operation to monitor the page, that is, to monitor finger lifting, rewrite dispatchTouchEvent can achieve our purpose, but when it is used in the existing project, we need to simplify the call logic, encapsulate the call so that the original business only needs to invoke Inject when it is enabled, and call onDestory when it ends. Make a good SDK.


Note: the article code has been uploaded to Github stamp I jump source view

We have opened the wechat public account “Code Nong Thatched Cottage”. If you are interested, you can follow it and learn together