安卓系统面经_安卓面经(14/20)输入系统
面试题预览
1. 请解释一下Android中的红外遥控的配置流程是怎样的?⭐⭐⭐⭐⭐
2. 请解释一下Android中的蓝牙遥控的配置流程是怎样的?⭐⭐⭐⭐⭐
3. 请解释一下Android中的按键处理流程是怎样的?⭐⭐⭐⭐
4. 请解释一下Android中的输入法切换是如何实现的?⭐⭐⭐
5. 请解释一下Android中的输入法界面定制是如何实现的?⭐⭐⭐⭐
1 配置红外遥控
1.1 说明
1) 红外遥控主要有2种,按调制方式分为PWM和PPM,分别对应NEC和PHILIPS的RC-5、RC-6、RC-7等协议 2) 红外遥控信号传递的关键节点:红外码----linux scancode----android keycode
1.2 添加步骤
基于RK3399 9.0平台配置
1.2.1 配置红外码值
1) 修改dts,添加遥控配置。可配置多个遥控,每组遥控分别取名为ir_key1/ir_key2等。以ir_key5为例,然后分别填写其头码、红外码值以及对应的linux键值名
ir_key5 { rockchip,usercode = <0x4cb3>; rockchip,key_table = <0x23 KEY_POWER>, <0x72 KEY_VOLUMEUP>, <0x7E KEY_ZOOM_MINUS>, <0x63 KEY_ZOOM_PLUS>, <0x7f KEY_VOLUMEDOWN>, <0x35 KEY_UP>, <0x66 KEY_LEFT>, <0x31 KEY_ENTER>,
1.2.2 配置Linux keycode
1. 修改/kernel/include/uapi/linux/input-event-codes.h,该文件定义了对应的linux层键值
#define KEY_MUTE 113 #define KEY_VOLUMEDOWN 114 #define KEY_VOLUMEUP 115 #define KEY_POWER 116
2. 修改kl,该文件定义了linux和android层之间键值映射的关系,如上0x2f1即等于十进制的753
key 113 VOLUME_MUTE key 114 VOLUME_DOWN key 115 VOLUME_UP key 116 DIR_POWER
3. kl文件的名称由该输入设备名称来决定。可输入“dumpsys input”命令来查看,如下图所示。
我们通过getevent可以知道遥控器的event编号为0,根据Path字段来映射,发现其设备名称 为ff420030.pwm,则其会优先解析使用ff420030_pwm.kl;若该文件不存在或文件内容不正 确,则会使用Vendor, Product,Version组合而成的kl文件(如Vendor_005d_Product_0001.kl);若仍旧不存在,则会使用系统自带的Generic.kl文件
1.2.3-配置android keycode
修改/frameworks/base/core/java/android/view/KeyEvent.java,最终Android上层即可接收到该值的 按键
public static final int KEYCODE_ZOOM_PLUS = 295;
public static final int KEYCODE_ZOOM_MINUS = 296;
public static final int KEYCODE_MIC_CTL = 297;
public static final int KEYCODE_AI = 298;
public static final int KEYCODE_ENDCALL = 299;
public static final int KEYCODE_CALL = 300;
public static final int KEYCODE_DIR_POWER = 5000;
1.2.4-其他
当更改了framwork层的api或是按键键值后,都需要更新到此文件
1.frameworks/base/api/current.txt
field public static final int KEYCODE_AI = 298; // 0x12a field public static final int KEYCODE_CALL = 300; // 0x12c field public static final int KEYCODE_ENDCALL = 299; // 0x12b field public static final int KEYCODE_MIC_CTL = 297; // 0x129 field public static final int KEYCODE_ZOOM_MINUS = 296; // 0x128 field public static final int KEYCODE_ZOOM_PLUS = 295; // 0x127 field public static final int KEYCODE_DIR_POWER = 5000;
2. frameworks/base/core/res/res/values/attrs.xml
3. frameworks/native/include/android/keycodes.h
AKEYCODE_ZOOM_PLUS = 295, AKEYCODE_ZOOM_MINUS = 296, AKEYCODE_MIC_CTL = 297, AKEYCODE_AI = 298, AKEYCODE_ENDCALL = 299, AKEYCODE_CALL = 300, AKEYCODE_DIR_POWER = 5000,
4. frameworks/native/include/input/InputEventLabels.h
DEFINE_KEYCODE(MAXHUB_POWER), DEFINE_KEYCODE(MAXHUB_MINI_ZOOM_PLUS), DEFINE_KEYCODE(MAXHUB_MINI_ZOOM_MINUS), DEFINE_KEYCODE(MAXHUB_MINI_MIC_CTL), DEFINE_KEYCODE(MAXHUB_MINI_AI), DEFINE_KEYCODE(MAXHUB_MINI_ENDCALL), DEFINE_KEYCODE(MAXHUB_MINI_CALL), DEFINE_KEYCODE(SEEWO_POWER),
1.3 调试方法
1.3.1-确认IR驱动安装成功
开机后,查看串口打印,有如下内容即安装成功
[ 1.729169] input: ff420030.pwm as /devices/platform/ff420030.pwm/input/input0
1.3.2-确认红外码值识别准确
1.3.2.1. 串口命令查看红外码值(不要关闭kernel打印,否则看不到)
#使能码值打印 echo 1 > /sys/module/rockchip_pwm_remotectl/parameters/code_print <enum name="KEYCODE_ZOOM_PLUS" value="295" /> <enum name="KEYCODE_ZOOM_MINUS" value="296" /> <enum name="KEYCODE_MIC_CTL" value="297" /> <enum name="KEYCODE_AI" value="298" /> <enum name="KEYCODE_ENDCALL" value="299" /> <enum name="KEYCODE_CALL" value="300" /> <enum name="KEYCODE_MAXHUB_RECOVERY" value="295" /> <enum name="KEYCODE_DIR_POWER" value="5000" />#按键后即有打印 130|console:/ # [ 296.206852] USERCODE=0x4cb3 [ 296.233841] RMC_GETDATA=23
1.3.2.2. 查看板卡中的kl文件,防止键值映射错误或配置错遥控
-rw-r--r-- 1 root root 1994 2020-12-07 11:32 Vendor_22b8_Product_093d.kl -rw-r--r-- 1 root root 968 2020-12-07 11:32 Vendor_2378_Product_1008.kl -rw-r--r-- 1 root root 955 2020-12-07 11:32 Vendor_2378_Product_100a.kl -rw-r--r-- 1 root root 4800 2020-12-07 11:32 ff420030_pwm.kl -rw-r--r-- 1 root root 796 2020-12-07 11:32 ff680030_pwm.kl -rw-r--r-- 1 root root 2410 2020-12-07 11:32 qwerty.kl -rw-r--r-- 1 root root 176 2020-12-07 11:32 rk29-keypad.kl console:/system/usr/keylayout #
1.3.2.3. 确认连续码是否可用。RK的红外遥控解码使用的是软解,且一些关键波形的周期卡的很严,适配一 些遥控时,很有可能在连续码上会出现异常。此时可放开驱动打印来判别是哪个周期除出了问题来调试
#使能关键波形周期的打印
echo 1 > sys/module/rockchip_pwm_remotectl/parameters/dbg_level
1. 示例:适配某款蓝牙红外一体遥控,发现连续码不能识别;放开打印,发现
是RK_PWM_TIME_SEQ1_MAX阈值太小导致
1.3.2.4.确认长按是否生效
#logcat -c
#logcat | grep -i keycode
看到repeatCount
在长按的时候有在不断累加即为正常
1.3.3-确认linux keycode正确
串口输入命令,查看对应的event事件,中间的数值即为linux keycode,最后的数值1/0分别表示按下/弹起
console:/system/usr/keylayout # getevent add device 1: /dev/input/event8 name: "cvte_touchscreen" add device 2: /dev/input/event7 name: "cvte_mouse" add device 3: /dev/input/event6 name: "cvte_keyboard" add device 4: /dev/input/event5 name: "TC02B_4MIC_3fd2_V0.0.0.0017 Built-in Audio" add device 5: /dev/input/event4 name: "ILITEK Touch Device,21.5-10P" add device 6: /dev/input/event1 name: "rk29-keypad" add device 7: /dev/input/event3 name: "gyro sensor" add device 8: /dev/input/event0 name: "ff420030.pwm" add device 9: /dev/input/event2 name: "accel sensor" /dev/input/event0: 0001 0074 00000001 /dev/input/event0: 0000 0000 00000000 /dev/input/event0: 0001 0074 00000000 /dev/input/event0: 0000 0000 00000000
1.3.4-确认android keycode正确
1. 使用logcat抓取打印 12-07 11:43:03.091 521 622 D WindowManager: interceptKeyTq keycode=5000 interactive=true keyguardActive=false policyFlags=22000000 2. 由于android framwork最先处理键值的是interceptKeyBeforeQueueing函数,其内有如下打印 //PhoneWindowManager.java if (DEBUG_INPUT) { Log.d(TAG, "interceptKeyTq keycode=" + keyCode + " interactive=" + interactive + " keyguardActive=" + keyguardActive + " policyFlags=" + Integer.toHexString(policyFlags)); }
1.3.5-确认应用收到键值
1. 如果framwork中没有拦截处理,一般会抛给上层应用来处理,抓取应用本身的打印即可 2. 也可以模拟发送keycode来测试app input keyevent 3 //HOME input keyevent 4 //BACK input keyevent 23 //DPAD_CENTER input keyevent 66 //ENTER
1.4 NEC红外遥控协议的知识
NEC连续码
针对红外码,一般企标要求是精度做到±20%,可以对比标准和波形来实际调试
1.5 注意事项
1. 一般不要直接修改linux层键值,有些系统的原生键值会有特殊用处,不能修改(例如3399平台只
能识别原生linux层power键值来遥控开机)
2. https://www.cnblogs.com/klb561/p/11029446.html
2 配置蓝牙遥控
2.1 蓝牙遥控器的原理
蓝牙遥控器的原理如下图
从流程上我们可以发现,我们遥控器的码值被转化了几次: HID码值--→Linux event--→ 根据Vendor、Product转化为Android事件。
2.2 蓝牙遥控按键知识
将蓝牙遥控与Android设备机子连接后,可以通过getevent 命令获取按键输入事件:
从上图可以知道
● 名字:蓝牙遥控器的名字为 BT_Smart_RC001
● 007004a:其中,高位为 usage page (07代表普通健,0c 代码多媒体健);低位为健值
● 0066:表示down的值,这个后面需要在 kl 文件中,转换成十进制的值
2.3 修改已有按键值
例如:某个遥控,按下Back 建,HOME 建不起作用。
先使用 getevent 按下Back健,拿到event 的值
这种通用的按键,在KeyEvent 是有的,我们要做的,就是修改 kl 的值即可。
使用 dumpsys input 命令拿到 kl 的位置:
可以看到 kl 在 Android 系统映射的位置,修改里面的值,0x9e 转成十进制为 158,同理拿到HOME的值,修改如下
修改后重启就发现已经起作用了。
2.4添加新的按键值
例如某个遥控有个按键,键值为0X59,我们想把它定义为新的健,所以需要走一遍按键添加流程;
2.4.1 添加键值和上层映射
方法一:HID_UP_CONSUMER类型
去到 linux 映射表,hid-input.c ,位置在 kernel/drivers/hid/hid-input.c ,去到 HID_UP_CONSUMER 这个方法,把0x59 添加进去,并新增 KEY_SOURCE 这个字符串,后续给Android使用的
方法二:HID_UP_KEYBOARD类型
去到 linux 映射表,hid-input.c ,位置在 kernel/drivers/hid/hid-input.c ,去到 HID_UP_KEYBOARD这个方法。
可以见到里面keyboard类型有个表格映射的。
因此可以根据蓝牙键值的Usage ID去修改给到Keylayout的Linux ScanCode。例如Usage ID为0x96,那么对应到表格的第十行第七列(表格是十六进制形式的,每行从左到右以此是0~F,列也是),那么可以修改第十行第七列的值为你定义的值(我这里定义的是0x2ea),那么送到Keylayout中这个按键的Linux code就是0x2ea,然后在转成Android KeyEvent即可。
2.4.2 头文件中添加字符串
去到 common/include/uapi/linux/input-event-codes.h ,添加刚才的KEY_SOURCE,注意不要重复即可
2.4.3 增加Android KeyCode的定义
在framework 的 KeyCodes.h , InputEventLabels.h 和KeyEvent.java
去到 frameworks/native/include/android ,在 KeyCodes.h 增加刚才的SOURCE,注意不要重复即可
然后再去到 frameworks/native/include/input,修改 InputEventLabels.h,定义刚才的source
最后,再去到 frameworks/base/core/java/android/view , 修改给Android 上层使用的 KeyEvent.java 即可:
这里,我们的按键值,就从 linux 传到 KeyEvent.java 了。
2.4.4 PhoneWindowManager 添加按键功能
接下来就是处理自己的功能了,此时按键已经通过 onKeyEvent()拿到,如果你想全局处理,可以在 PhoneWindowManager 的 interceptKeyBeforeQueueing方法添加功能,如:
这样,新建一个按键就完成了
3 Android按键处理流程
上面介绍了怎么添加红外和蓝牙遥控,本节着重结合源码讲述Android的按键处理流程,下面以最常见的Power为例
3.1 简介
Android系统中,一般的按键都可以在应用中处理,但是,对于系统级别的按键上层应用是无法收到消息的,也就是说,你的APP是无法直接处理的。针对这种系统级的按键事件,都是在Event事件分发前处理。Event事件分发后,只有包含有Activity的APP才能处理事件;若APP无法处理,则需要在PhoneWindowManager中处理。
本文所讲的Power键则属于该种情况。即用户触发Power键,底层收到按键会回调InputMonitor的函数dispatchUnhandledKey()。
3.2 按键处理流程
看到上面的简介,是否会有这样的疑问。为何最终处理者是PhoneWindowManager?
通过上文可知最终事件的处理是由PhoneWindowManager完成的;那么,按键后,系统是如何传递到PhoneWindowManager?下面就从源码的角度分析一下该过程。
● WindowManagerService:Framework 最核心的服务之一,负责窗口管理。
● InputManagerService:输入管理服务。
上述两个服务与Power按键相关,但是两者是如何关联的,就要从它们的创建说起.我们都知道,Android系统中的核心进程是system_server,对应SystemServer类,在其run()方法中会启动一堆的service,当然包括上述两个服务。具体源码分析如下:
3.2.1 先创建inputManager,再创建WindowManagerService对象时,可发现作为参数引用了上述inputManager,且创建了PhoneWindowManager实例:
//源码路径:frameworks/base/services/java/com/android/server/SystemServer.javaprivate void startOtherServices() { ...... inputManager = new InputManagerService(context); //输入系统服务 【step_SystemServer_1】 ...... //【step_SystemServer_2】 wm = WindowManagerService.main(context, inputManager, mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL, !mFirstBoot, mOnlyCore, new PhoneWindowManager()); //PhoneWindowManager实例 ServiceManager.addService(Context.WINDOW_SERVICE, wm); ServiceManager.addService(Context.INPUT_SERVICE, inputManager); } //源码路径:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java private WindowManagerService(Context context, InputManagerService inputManager, boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore, WindowManagerPolicy policy) { ...... mPolicy = policy; //实例: PhoneWindowManager对象 【step_InputMonitor_2】 ......}
3.2.2 启动inputManager之前,设置了一个回调接口:
//消息分发之前回调--->查看InputManagerService inputManager.setWindowManagerCallbacks(wm.getInputMonitor()); inputManager.start();
3.2.3 InputMonitor.java底层收到按键会回调InputManagerService的dispatchUnhandledKey()--->InputMonitor的函数dispatchUnhandledKey()。具体由底层InputDispatcher.cpp调用。
//源码路径: frameworks/base/services/core/java/com/android/server/wm/InputMonitor.java //【Power键属于系统级按键,因此处理方法是dispatchUnhandledKey】 /* Provides an opportunity for the window manager policy to process a key that * the application did not handle. */ @Override public KeyEvent dispatchUnhandledKey( InputWindowHandle focus, KeyEvent event, int policyFlags) { WindowState windowState = focus != null ? (WindowState) focus.windowState : null; //此处 mservice: WindowManagerService 【step_InputMonitor_0】 return mService.mPolicy.dispatchUnhandledKey(windowState, event, policyFlags); }
由这三步可知,最终由PhoneWindowManager处理。将上述整理成时序图:
3.3 Power按键触发后的具体执行逻辑分析
列出几种常见的触发Power键的情况:
情况一:长按Power键
情况二:单独短按Power键
情况三:Power + 音量键(-)
以下也以这三种情况结合源码分析流程。
由上文可知,真正的处理逻辑在PhoneWindowManager类中,该类有两个方法:interceptKeyBeforeDispatching和interceptKeyBeforeQueueing,包括了几乎所有按键的处理。
● interceptKeyBeforeDispatching:主要处理Home键、Menu键、Search键等。
● interceptKeyBeforeQueueing:主要处理音量键、电源键(Power键)、耳机键等。
3.3.1 当前Power键处理流程:
dispatchUnhandledKey()------>interceptFallback()---->interceptKeyBeforeQueueing()
下面从interceptKeyBeforeQueueing(KeyEvent event, int policyFlags)分析。而一个按键包含两个动作Down和UP,因此从这两个方面分析interceptKeyBeforeQueueing()的执行流程。
● 按下: interceptPowerKeyDown(KeyEvent event, boolean interactive)
● 释放: interceptPowerKeyUp(KeyEvent event, boolean interactive, boolean canceled)
参数含义:
interactive:是否亮屏
KeyEvent.FLAG_FALLBACK:不被应用处理的按键事件或一些在 键值映射中不被处理的事件(例:轨迹球事件等)。
根据操作按键时系统是否亮屏,代码执行的逻辑也不同,因此每个事件下分别从亮灭屏来分析,具体如下:
(下述代码的执行过程中有对一些变量的判断,而这些值都是系统配置的,在config.xml中,因此具体执行哪个流程以当前平台配置为准)
涉及到的配置信息的相关源码路径: frameworks/base/core/java/android/view/ViewConfiguration.java frameworks/base/core/res/res/values/config.xml
1、 按下(ACTION_DOWN):先上源码PhoneWindowManager.java中的interceptPowerKeyDown函数。
private void interceptPowerKeyDown(KeyEvent event, boolean interactive) { //FACE_UNLOCK_SUPPORT start Slog.i("FaceUnlockUtil", "interceptPowerKeyDown interactive = " + interactive); Settings.System.putInt(mContext.getContentResolver(), "faceunlock_start", 1); //FACE_UNLOCK_SUPPORT end // Hold a wake lock until the power key is released. if (!mPowerKeyWakeLock.isHeld()) { mPowerKeyWakeLock.acquire(); //获得唤醒锁 } // Cancel multi-press detection timeout. if (mPowerKeyPressCounter != 0) { mHandler.removeMessages(MSG_POWER_DELAYED_PRESS); } // Detect user pressing the power button in panic when an application has // taken over the whole screen. boolean panic = mImmersiveModeConfirmation.onPowerKeyDown(interactive, SystemClock.elapsedRealtime(), isImmersiveMode(mLastSystemUiFlags), isNavBarEmpty(mLastSystemUiFlags)); if (panic) { mHandler.post(mHiddenNavPanic); } // Latch power key state to detect screenshot chord. if (interactive && !mScreenshotChordPowerKeyTriggered && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { mScreenshotChordPowerKeyTriggered = true; //标记按下power key,用于组合键截屏,具体参考下述3 mScreenshotChordPowerKeyTime = event.getDownTime(); interceptScreenshotChord(); } // Stop ringing or end call if configured to do so when power is pressed. TelecomManager telecomManager = getTelecommService(); boolean hungUp = false; if (telecomManager != null) { if (telecomManager.isRinging()) { // Pressing Power while there\'s a ringing incoming // call should silence the ringer. telecomManager.silenceRinger(); } else if ((mIncallPowerBehavior & Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0 && telecomManager.isInCall() && interactive) {
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
2020年研究生毕业后,工作重心由嵌入式Linux转为安卓系统,Android发展已经很多年,网上面向中初级Android系统开发的面经还比较少,也不够集中,因此梳理出本专栏,本专栏收集了本人工作中持续积累的众多安卓系统知识,持续更新中。