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
-
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.
-
How to notify: broadcast, EventBus/RxBus, or custom callback, if you can send the event back
-
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:
attach
Set it in Application just to get the context and broadcast itupdate
Update timecancel
Cancel 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