Android8.0中实现APP禁用模式(三)

上篇文章最后留了一个问题:就是如果禁用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的名字都能获取到。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值