上篇文章最后留了一个问题:就是如果禁用app的时刻,被禁用的APP是打开的或者是最近应用视图正在显示,该怎么处理?
我的解决方案是模拟一个HOME键的事件。这样,就能退出的状态,回到主页,同时给出提示,告知用户APP已被禁用。
这里就有两个问题了:1、如何模拟HOME按键。2、如何判断哪个APP在前台。
模拟按键
我们知道,用adb可以模拟按键,那么,我们就从input命令的实现入手。input的实现是在framework/base/cmds/input/src/com/android/commands/input/input.java。
public static void main(String[] args) {
(new Input()).run(args);
}
private void run(String[] args) {
...
try {
if (command.equals("text")) {
...
} else if (command.equals("keyevent")) {
if (length >= 2) {
final boolean longpress = "--longpress".equals(args[index + 1]);
final int start = longpress ? index + 2 : index + 1;
inputSource = getSource(inputSource, InputDevice.SOURCE_KEYBOARD);
if (length > start) {
for (int i = start; i < length; i++) {
int keyCode = KeyEvent.keyCodeFromString(args[i]);
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
keyCode = KeyEvent.keyCodeFromString("KEYCODE_" + args[i]);
}
sendKeyEvent(inputSource, keyCode, longpress);
}
return;
}
}
}
...
} catch (NumberFormatException ex) {
}
...
}
这里,通过getSource()函数获取inputSource,然后获取int类型的keyCode,再调用sendKeyEvent函数:
private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
long now = SystemClock.uptimeMillis();
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
if (longpress) {
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
inputSource));
}
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
}
这里调用injectKeyEvent()函数生成按下、长按和抬起事件:
private void injectKeyEvent(KeyEvent event) {
Log.i(TAG, "injectKeyEvent: " + event);
InputManager.getInstance().injectInputEvent(event,
InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
}
这里就是调用InputManager的injectInputEvent方法插入相应的事件了。所以,我们在APP里面也可以这么做。
InputManager中的injectInputEvent方法是hide的,所以想要在APP里调用,要么用反射,要么就用定制的android.jar。这里我使用反射来调用。
sendKeyEvent(InputDevice.SOURCE_KEYBOARD, KeyEvent.KEYCODE_HOME, false);
private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
long now = SystemClock.uptimeMillis();
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
if (longpress) {
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
inputSource));
}
injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
}
private void injectKeyEvent(KeyEvent event) {
Log.i(TAG, "injectKeyEvent: " + event);
InputManager im = (InputManager) getSystemService(Context.INPUT_SERVICE);
Method method;
try {
method = InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class);
method.invoke(im, event, 2);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
据说,模拟HOME键必须是系统应用,也就是声明uid为system,且有system签名的应用。
判断前台应用
网上有很多判断应用是否在前台的方法,都不是很直接,不太符合我的需求。我这里通过在ActivityManagerService中增加代码,以提供给APP前台应用的信息。
以前我做过通过在Instrumention这个类里面的callActivityOnResume方法里面增加广播,来通知APP前台应用有切换。但是在我们的这个需求里面,如果用广播的方式,那么APP就无法做查询,只能是在收到广播之后自己记录当前的前台应用。这么做不是很优雅。如果在Instrumention里面写系统属性或者Settings里面的值,因为Instrumention里面的两个context并不是system,会导致经常无法写值。
在ActivityManagerService中搜索resume,我找到了这几个方法:
@Override
public final void activityResumed(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized(this) {
ActivityRecord.activityResumedLocked(token);
mWindowManager.notifyAppResumedFinished(token);
}
Binder.restoreCallingIdentity(origId);
}
@Override
public final void activityPaused(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized(this) {
ActivityStack stack = ActivityRecord.getStackLocked(token);
if (stack != null) {
stack.activityPausedLocked(token, false);
}
}
Binder.restoreCallingIdentity(origId);
}
@Override
public final void activityStopped(IBinder token, Bundle icicle,
PersistableBundle persistentState, CharSequence description) {
if (DEBUG_ALL) Slog.v(TAG, "Activity stopped: token=" + token);
// Refuse possible leaked file descriptors
if (icicle != null && icicle.hasFileDescriptors()) {
throw new IllegalArgumentException("File descriptors passed in Bundle");
}
final long origId = Binder.clearCallingIdentity();
synchronized (this) {
final ActivityRecord r = ActivityRecord.isInStackLocked(token);
if (r != null) {
r.activityStoppedLocked(icicle, persistentState, description);
}
}
trimApplications();
Binder.restoreCallingIdentity(origId);
}
@Override
public final void activityDestroyed(IBinder token) {
if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "ACTIVITY DESTROYED: " + token);
synchronized (this) {
ActivityStack stack = ActivityRecord.getStackLocked(token);
if (stack != null) {
stack.activityDestroyedLocked(token, "activityDestroyed");
}
}
}
@Override
public final void activityRelaunched(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized (this) {
mStackSupervisor.activityRelaunchedLocked(token);
}
Binder.restoreCallingIdentity(origId);
}
从函数名来看,这几个函数应该与Activity声明周期对应的,所以,我在这里增加日志跑了一下,发现确实如此。而且,Service里面的context是system的,能用来写属性和Settings。那么,我们就在activityResumed()方法里面增加写系统属性或者Settings的代码。
但是,当我写Settings的时候,报了context为空。所以,最终我写的是系统属性。
activityResumed()方法的参数类型是IBinder,怎么通过IBinder获取到包名呢?在ActivityManagerService里面搜索一下是package,就找到了下面两个方法:
@Override
public ComponentName getActivityClassForToken(IBinder token) {
synchronized(this) {
ActivityRecord r = ActivityRecord.isInStackLocked(token);
if (r == null) {
return null;
}
return r.intent.getComponent();
}
}
@Override
public String getPackageForToken(IBinder token) {
synchronized(this) {
ActivityRecord r = ActivityRecord.isInStackLocked(token);
if (r == null) {
return null;
}
return r.packageName;
}
}
通过这两个方法,不光能获取到包名,连Activity的名字都能获取到。