一、背景
Android 中如何屏蔽第三方SDK中的Activity的返回事件?假设业务中集成了一款SDK,在这个SDK内部有一个的Activity页面。在业务中可以通过调用SDK提供的接口启动三方SDK中Activity。
假设这个Activity是展示广告的,涉及到应用的收入,那么产品经理就希望这个页面展示的时间长一些,可以通过服务端配置的方式,动态调整用户可以几秒之后通过返回键关闭这个页面。
在无法修改SDK源码的情况下,如何屏蔽这个广告Activity的返回事件,在规定时间内,不允许用户通过返回键关闭该页面。
二、问题解法
1、明确问题
摆在我们面前的有一个重要的问题,写在SDK中的Activity的代码我们是改不了的。
Android 开发同学都知道,如果可以改源码我们可以在Activity中,通过重写 onBackPressed()控制不让用户通过返回键关闭页面。那么当这个Activity是SDK中的代码,不允许我们更改时,我们在外部可以怎么做呢?
2、Activity中onBackPressed()是如何被调用的?
首先我们可以看看Activity中的onBackPressed()方法是如何被调用的。
直接在Activity中搜索onBackPressed(),可以发现在onKeyUp()中,调用了该方法。
public boolean onKeyUp(int keyCode, KeyEvent event) {
int sdkVersion = getApplicationInfo().targetSdkVersion;
if (sdkVersion >= Build.VERSION_CODES.ECLAIR) {
if (keyCode == KeyEvent.KEYCODE_BACK
&& event.isTracking()
&& !event.isCanceled()
&& mDefaultBackCallback == null) {
// Using legacy back handling.
onBackPressed();
return true;
}
}
return false;
}
在onKeyUp()方法中,判断了当keyCode为KeyEvent.KEYCODE_BACK 时,则调用onBackPressed()的方法。
那我们进一步看看onKeyUp是在哪里被调用的。最终可以看到是在KeyEvent.dispatch()中被调用
public final boolean dispatch(Callback receiver, DispatcherState state,
Object target) {
switch (mAction) {
case ACTION_DOWN: {
mFlags &= ~FLAG_START_TRACKING;
if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
+ ": " + this);
boolean res = receiver.onKeyDown(mKeyCode, this);
if (state != null) {
if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
if (DEBUG) Log.v(TAG, " Start tracking!");
state.startTracking(this, target);
} else if (isLongPress() && state.isTracking(this)) {
try {
if (receiver.onKeyLongPress(mKeyCode, this)) {
if (DEBUG) Log.v(TAG, " Clear from long press!");
state.performedLongPress(this);
res = true;
}
} catch (AbstractMethodError e) {
}
}
}
return res;
}
case ACTION_UP:
if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
+ ": " + this);
if (state != null) {
state.handleUpEvent(this);
}
return receiver.onKeyUp(mKeyCode, this);
case ACTION_MULTIPLE:
final int count = mRepeatCount;
final int code = mKeyCode;
if (receiver.onKeyMultiple(code, count, this)) {
return true;
}
if (code != KeyEvent.KEYCODE_UNKNOWN) {
mAction = ACTION_DOWN;
mRepeatCount = 0;
boolean handled = receiver.onKeyDown(code, this);
if (handled) {
mAction = ACTION_UP;
receiver.onKeyUp(code, this);
}
mAction = ACTION_MULTIPLE;
mRepeatCount = count;
return handled;
}
return false;
}
return false;
}
KeyEvent.dispatch()则是在Activity dispatchKeyEvent()中被调用
public boolean dispatchKeyEvent(KeyEvent event) {
onUserInteraction();
// Let action bars open menus in response to the menu key prioritized over
// the window handling it
final int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_MENU &&
mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
return true;
}
Window win = getWindow();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
dispatchKeyEvent() 方法则是Window.Callback接口的方法。
Callback接口声明:
public interface Callback {
/**
* Called to process key events. At the very least your
* implementation must call
* {@link android.view.Window#superDispatchKeyEvent} to do the
* standard key processing.
*
* @param event The key event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchKeyEvent(KeyEvent event);
/**
* Called to process touch screen events. At the very least your
* implementation must call
* {@link android.view.Window#superDispatchTouchEvent} to do the
* standard touch screen processing.
*
* @param event The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent event);
...
}
Window中存在 setCallback() 接口
/**
* Set the Callback interface for this window, used to intercept key
* events and other dynamic operations in the window.
*
* @param callback The desired Callback interface.
*/
public void setCallback(Callback callback) {
mCallback = callback;
}
我们查看Activity可以看到其实现了Callback接口:
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
ContentCaptureManager.ContentCaptureClient {
...
}
在Activity中的attach方法中调用了mWindow.setCallback(this);
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
...
}
至此整个链路比较清晰了。事件是通过Windown内的Callback对象传递给Activity的,如果我们需要拦截返回事件,那么可以通过自定义Callback对象,替换掉Window中的Callback对象的方式拦截事件。
至此问题就变成了如何获取Window对象了,想要获取Window对象,就要先获取到这个页面的Activity对象。因为Activity中存在 getWindow()
方法。
然后问题就又转为如何获取到三方SDK中页面的Activity对象。
由于我们知道三方SDK中Activity的ClassName,那么我们可以先通过ActivityLifecycleCallbacks接口,在其onCreate()回调时,获取其Activity对象。
getApplication().registerActivityLifecycleCallbacks()
3、自定义Window.Callback
我们先通过Activity.getWindow()
获取到Activity中的Window对象。这里Wondow接口其实是PhoneWindow对象。
我们先调用Window的 getCallback() 方法,获取window对象中原本设置的CallBack,然后调用 setCallback(),设置我们的CallBack包装类,将window对象中的原CallBack对象作为参数传递进去。
示例代码
class CustomWindowCallback(private val originCallBack: Window.Callback): Window.Callback {
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
return originCallBack.dispatchKeyEvent(event)
}
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
return originCallBack.dispatchTouchEvent(event)
}
override fun dispatchKeyShortcutEvent(event: KeyEvent?): Boolean {
return originCallBack.dispatchKeyShortcutEvent(event)
}
...
}
此时我们可以在dispatchKeyEvent()方法中判断当前的时间是返回键,然后消费该事件。
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
val keyCode = event?.keyCode
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Using legacy back handling.
return true
}
return originCallBack.dispatchKeyEvent(event)
}
这样我们就做到了拦截某个Activity的返回事件。至此我们就做到了,在外部拦截三方SDK中Activity的返回键。实际上基于此我们可以发散一下,由于我们包装了Callback接口,Callback中还存在如dispatchTouchEvent()
我们可以拦截任何传递给Activity的任何事件了。
4、其他思路
除了代理Window.Callback,也可以考虑通过Hook的方式来做,由于不是本篇重点,这里就不做详细介绍了。