阅读Android AOSP 12版本代码,对输入法IME整体框架模块进行学习梳理,内容包含输入法框架三部分IMM、IMMS、IMS的启动流程、点击弹出流程、显示/隐藏流程,以及常见问题和调试技巧。
1. IME整体框架
IME整体分为三个部分:
1.1. 输入法客户端(IMM)
代码路径:frameworks/base/core/java/android/view/inputmethod/
主要指输入法框架的InputMethodManager, 每个app都一个实例, 用来和输入法控制端交互。运行在需要使用输入法的进程
1.2. 输入法管理端(IMMS)
代码路径:frameworks/base/services/core/java/com/android/server/inputmethod/
主要指输入法框架的InputMethodManagerService, 运行在system_server进程,工作内容包含以下:
- 输入法服务端的绑定
- 输入法服务端与输入法客户端的绑定
- 输入法启用/关闭
- 输入法显示/隐藏
- 切换输入法
1.3. 输入法服务端(IMS)
代码路径:frameworks/base/core/java/android/inputmethodservice/
主要指输入法框架的InputMethodService, 这是一个输入法服务, 真正实现输入法界面, 控制字符输入的地方。运行在输入法进程, 例如某狗输入法进程
1.4. 子模块交互流程图
输入法的整体交互过程如下:
- IMM利用IInputMethodManager请求IMMS
- IMMS绑定输入法服务InputMethodService, 得到IInputMethod
- IMMS请求IInputMethod创建交互IInputMethodSession
- IMMS通过IInputMethodClient告知IMM IInputMethodSession
- IMM和IMS通过IInputMethodSession和IInputContext交互
2. IME初始化启动流程
2.1. IME客户端(IMM)初始化流程
涉及代码文件路径: frameworks/base/core/java/android/view/ViewRootImpl.java frameworks/base/core/java/android/view/WindowManagerGlobal.java frameworks/base/core/java/android/view/inputmethod/InputMethodManager.java frameworks/base/core/java/com/android/internal/view/IInputMethodClient.aidl frameworks/base/core/java/com/android/internal/view/IInputContext.aidl frameworks/base/core/java/com/android/internal/view/IInputMethodManager.aidl frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
2.1.1. 函数流程梳理
# 每次新增窗口window时,都会实例化ViewRootImpl,而ViewRootImpl在获取IWindowSession时会检查输入法是否已经初始化
ViewRootImpl.java -- 初始化构造函数,调用WindowManagerGlobal.getWindowSession()---> WindowManagerGlobal.java -- getWindowSession()调用InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary() 实例化全局调用InputMethodManager,即初始化IMM---> InputMethodManager.java -- ensureDefaultInstanceForDefaultDisplayIfNecessary()调用forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper()),入参默认displayID和looper# 此处也说明,对于APP层,IMM有且只有一个实例,每次创建ViewRootImpl都会检查IMM是否实例化完成---》 调用forContextInternal函数,先从缓存Map中查询是否有IMM实例,如果没有则创建IMM实例,并添加到Map中---》 调用createInstance创建实例,然后在三目运算中默认固定调用createRealInstance(displayId, looper)---》 调用createRealInstance函数, (1)获取输入法服务service,即Context.INPUT_METHOD_SERVICE(service是AIDL接口文件IInputMethodManager.aidl);(2)new InputMethodManager(service, displayId, looper)创建实例---》 InputMethodManager构造函数---》 new IInputConnectionWrapper 创建虚拟的输入法上下文,主要用于监听输入法服务的激活状态,接受输入事件# 添加IMM实例到输入法service服务中# 此处两个入参都是AIDL接口类型的对象# (1)IInputMethodClient.aidl:输入法客户端, 主要用于报告输入法当前的状态, 让APP应用端的IMM做出相应的处理# (2)IInputContext.aidl:输入法上下文, 主要用于操作字符输入操作, 让当前接收字符的view进行处理(3)调用service.addClient(imm.mClient //[AIDL对象,即IInputMethodClient], imm.mIInputContext//[AIDL对象,IInputContext], displayId)---> IInputMethodManager.aidl -- 调用addClient(跨进程通信到IMMS)---> 服务端InputMethodManagerService.java "extends IInputMethodManager.Stub" -- 调用addClient函数,创建ClientState对象---》 调用内部静态类ClientState的构造函数,保存client相关状态属性
综上代码流程梳理,可以看出:
- 对于每个APP应用,IMM有且只有一个实例,并且每次创建ViewRootImpl时,都会检查IMM是否已经实例化成功
- 实例化IMM对象时,会涉及到两个AIDL接口文件,一个用于应用端IMM处理输入法当前状态,一个用于输入法上下文,创建一个虚拟的InputContext代表输入空间,用于监听输入法激活状态
- 实例化过程中会有个displayid,用于多屏幕显示(通常情况下默认是default display=0)
- 实例化最后,会通过AIDL的addClient接口函数,将IMM添加到IMMS中,如此IMM实例化完成
2.1.2. 代码详细说明
//ViewRootImpl.javapublic ViewRootImpl(Context context, Display display) {this(context, display, WindowManagerGlobal.getWindowSession(),false /* useSfChoreographer */);}//WindowManagerGlobal.javapublic static IWindowSession getWindowSession() {synchronized (WindowManagerGlobal.class) {if (sWindowSession == null) {try {//调用该函数,初始化IMMInputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();......} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}return sWindowSession;}}//InputMethodManager.javapublic static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {//默认default displayforContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());}private static InputMethodManager forContextInternal(int displayId, Looper looper) {final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;synchronized (sLock) {//从缓存Map中查找是否由default display的IMM实例InputMethodManager instance = sInstanceMap.get(displayId);//如果存在实例,则直接返回if (instance != null) {return instance;}//初始化创建实例instance = createInstance(displayId, looper);//如果是用于default display使用,则存储到sInstance中作为全局单例实例if (sInstance == null && isDefaultDisplay) {sInstance = instance;}//将IMM实例保存到Map中sInstanceMap.put(displayId, instance);return instance;}}private static InputMethodManager createInstance(int displayId, Looper looper) {//isInEditMode固定返回false,直接调用createRealInstancereturn isInEditMode() ? createStubInstance(displayId, looper): createRealInstance(displayId, looper);}private static InputMethodManager createRealInstance(int displayId, Looper looper) {//IInputMethodManager是AIDL接口文件,用于跨进程通信到IMMS(InputMethodManagerService)final IInputMethodManager service;try {//获取serviceservice = IInputMethodManager.Stub.asInterface(ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));} catch (ServiceNotFoundException e) {throw new IllegalStateException(e);}//创建IMM实例final InputMethodManager imm = new InputMethodManager(service, displayId, looper);//将PID/UID和每个IME客户端关联,然后作为跨进程服务端IPC使用梳理//如果作为同进程内调用梳理,则需要确保Binder.getCalling{Pid, Uid}()返回Process.my{Pid, Uid}()//无论哪种情况,都要调用Binder的{clear, restore}CallingIdentity()函数,对跨进程没有影响,对同进程可以满足需求实现final long identity = Binder.clearCallingIdentity();try {// 添加 IMM 实例到输入法服务// imm.mClient 是一个aidl对象, mClient即new IInputMethodClient.Stub(),AIDL接口// imm.mIInputContext 是一个aidl对象, IInputContext,AIDL接口service.addClient(imm.mClient, imm.mIInputContext, displayId);} catch (RemoteException e) {e.rethrowFromSystemServer();} finally {Binder.restoreCallingIdentity(identity);}return imm;}//InputMethodManagerService.java//由每个APP应用进程调用,作为输入法开始与交互的准备@Overridepublic void addClient(IInputMethodClient client, IInputContext inputContext,int selfReportedDisplayId) {//获取调用的uid和pid(即InputMethodManager实际运行所在的UID/PID)//两种情况下调用此方法://1.IMM正在另一个进程中实例化//2.IMM正在同一个进程中实例化,final int callerUid = Binder.getCallingUid();final int callerPid = Binder.getCallingPid();synchronized (mMethodMap) {// TODO: Optimize this linear search.final int numClients = mClients.size();for (int i = 0; i < numClients; ++i) {final ClientState state = mClients.valueAt(i);if (state.uid == callerUid && state.pid == callerPid&& state.selfReportedDisplayId == selfReportedDisplayId) {throw new SecurityException("uid=" + callerUid + "/pid=" + callerPid+ "/displayId=" + selfReportedDisplayId + " is already registered.");}}//利用IBinder.deathRecipient监听client存活状态//如果client的Binder死亡,则将Client从缓存Map中移除final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);try {client.asBinder().linkToDeath(deathRecipient, 0);} catch (RemoteException e) {throw new IllegalStateException(e);}//此处不验证displayID,后续每当客户端需要与指定的交互时,就需要检查displayID//此处创建ClientState对象,将client和inputContext缓存进去,然后将该对象保存到缓存Map mClients中mClients.put(client.asBinder(), new ClientState(client, inputContext, callerUid,callerPid, selfReportedDisplayId, deathRecipient));}}
2.1.3. IMM初始化序列图
2.2. IME管理端(IMMS)初始化流程
IMMS运行在system server进程中,属于系统服务的一部分,用于控制输入法的显示/隐藏、切换、绑定等操作。
涉及代码文件路径: frameworks/base/services/java/com/android/server/SystemServer.java frameworks/base/services/core/java/com/android/server/SystemServiceManager.java frameworks/base/core/java/android/os/SystemService.java frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java frameworks/base/packages/SettingsProvider/res/values/defaults.xml
2.2.1. 初始化函数流程梳理
# 我们从systemserver的startOtherServices函数开始梳理
# 此处需要注意,因为我梳理的是IMMS,而Google还提供了一个MultiClientInputMethodManagerService多客户端输入法服务进程,此处不梳理
# PS:从InputMethodManagerService代码文件中可以看到,Lifecycle是里面的一个内部类,继承systemservice
SystemServer.java -- startOtherServices,然后通过SystemServiceManager的startService启动IMMS,传入class name:"InputMethodManagerService.Lifecycle.class"---> SystemServiceManager.java -- startService有好几个重载的方法,说明下:(1)第一个startService方法,入参className即"InputMethodManagerService.Lifecycle.class",将其作为入参调用loadClassFromLoader(2)loadClassFromLoader会通过反射方法得到具体的Class类,返回Class<SystemService>类型的服务类,即继承SystemService的Lifecycle(3)调用第二个startService方法,入参即serviceClass(4)先通过"SystemService.class.isAssignableFrom(serviceClass)"判断该类是否是SysteService的子类(5)然后通过反射构造类的实例"service=constructor.newInstance(mContext)",即实例化Lifecycle类(重点)(6)调用第三个startService方法,入参该Lifecycle对象(2)先将该service添加到mServices列表中,然后调用SystemService.java的onStart函数---> InputMethodManagerService.java -- 通过上面的流程看到,此处会先调用Lifecycle类的构造函数,然后调用onStart函数(1)构造函数会创建IMMS实例,即"InputMethodManagerService mService=new InputMethodManagerService(context)"(2)onStart函数会将该mService通过publishBinderService方法发布到系统服务中,以便其他进行可以进行Binder获取到(即添加到dev/binder域管理)# 主要讲述IMMS对象被创建,从构造函数梳理---》 调用构造函数,主要用于注册一些监听事件, 获取必须的系统服务, UI相关的组件等
PS:
- SystemService启动输入法服务时,会有个判断启动IMMS还是MCIMMS。MULTI_CLIENT_IME_ENABLED(即
persist.debug.multi_client_ime
或ro.sys.multi_client_ime
)开启,启动MultiClientInputMethodManagerService服务,否则启动InputMethodManagerService服务 - 关于MultiClientInputMethodManagerService就是多会话输入法,支持每屏幕焦点是启用此功能的前提。如果不支持,则无法启用此功能。由于安全限制,每屏幕焦点限制规定只有一小部分设备支持此功能。(详细参考Google官方文档和源码)
2.2.2. systemRunning函数流程梳理
# 我们从systemserver的startOtherServices函数开始梳理
# startBootPhase在服务startservice后执行,该函数将service分段处理,
# 例如此处IMMS在SystemService.PHASE_WAIT_FOR_SENSOR_SERVICE(200)和SystemService.PHASE_LOCK_SETTINGS_READY(480)之间
SystemServer.java -- startOtherServices,然后通过SystemServiceManager的startBootPhase---> SystemServiceManager.java -- startBootPhase遍历两个分段之间的服务,然后调用对应service的onBootPhase---> InputMethodManagerService.java -- 调用Lifecycle类的onBootPhase函数,然后调用InputMethodManagerService的systemRunning函数,主要内容:(1)MyPackageMonitor内部类register注册,监听安装包的变化,包含安装,卸载,更新等(2)SettingsObserver注册,监听当前用户的各种输入法相关的settingprovider变化,例如:默认输入法,输入法列表,输入法语言等(3)getSelectedInputMethod获取用户设置的输入法default_input_method,此处是查询settings数据库的默认输入法(frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java)(4)buildInputMethodListLocked,如果没有默认输入法则入参false,该函数内容如下:---》 查询输入法服务信息,然后将信息储存到mMethodList,mMethodMap,mMyPackageMonitor中;---》 调用chooseNewDefaultIMELocked选择一个新的输入法;---》 updateInputMethodsFromSettingsLocked遍历所有输入法,如果输入法存在被禁用的组件,则重新启用调用setInputMethodLocked方法完成对输入法设置,和输入法发生变化的广播(ACTION_INPUT_METHOD_CHANGED)的发送(该函数中调用setInputMethodLocked)
PS:一般我们修改默认输入法,packages/SettingsProvider/res/values/defaults.xml
数据库配置添加def_input_method
和def_enable_input_methods
,然后frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
对应添加loadStringSetting加载引用DEFAULT_INPUT_METHOD
和ENABLED_INPUT_METHODS
2.2.3. 代码详细说明
//SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {......// Bring up services needed for UI.if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {t.traceBegin("StartInputMethodManagerLifecycle");if (InputMethodSystemProperty.MULTI_CLIENT_IME_ENABLED) {mSystemServiceManager.startService(MultiClientInputMethodManagerService.Lifecycle.class);} else {//启动IMMS服务mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);}t.traceEnd();......}...
}//SystemServiceManager.java//第一个startService函数public SystemService startService(String className) {//调用loadClassFromLoaderfinal Class<SystemService> serviceClass = loadClassFromLoader(className,this.getClass().getClassLoader());return startService(serviceClass);}private static Class<SystemService> loadClassFromLoader(String className,ClassLoader classLoader) {try {//通过反射方法得到具体的Class类,返回Class<SystemService>类型的服务类,即继承SystemService的Lifecyclereturn (Class<SystemService>) Class.forName(className, true, classLoader);} catch (ClassNotFoundException ex) {.......}}//第二个startService函数public <T extends SystemService> T startService(Class<T> serviceClass) {try {final String name = serviceClass.getName();Slog.i(TAG, "Starting " + name);Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "StartService " + name);// 判断该class该类是否是SysteService的子类if (!SystemService.class.isAssignableFrom(serviceClass)) {throw new RuntimeException("Failed to create " + name+ ": service must extend " + SystemService.class.getName());}final T service;try {//通过反射构造类的实例,即实例化Lifecycle类Constructor<T> constructor = serviceClass.getConstructor(Context.class);//newInstance实例化service = constructor.newInstance(mContext);} catch (InstantiationException ex) {throw new RuntimeException("Failed to create service " + name+ ": service could not be instantiated", ex);} ............//调用第三个startServicestartService(service);return service;} finally {Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);}}//第三个startService函数public void startService(@NonNull final SystemService service) {// Register it.将service注册到mServices列表中mServices.add(service);// Start it.long time = SystemClock.elapsedRealtime();try {//调用该service的onStart函数service.onStart();} catch (RuntimeException ex) {throw new RuntimeException("Failed to start service " + service.getClass().getName()+ ": onStart threw an exception", ex);}warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");}//InputMethodManagerService.javapublic static final class Lifecycle extends SystemService {private InputMethodManagerService mService;//实例化时调用构造函数public Lifecycle(Context context) {super(context);//创建InputMethodManagerService IMMS对象,然后调用IMMS构造函数mService = new InputMethodManagerService(context);}//在startService中调用到此处@Overridepublic void onStart() {//将IMMS service添加到LocalServicesLocalServices.addService(InputMethodManagerInternal.class,new LocalServiceImpl(mService));//发布到系统服务中,以便其他进行可以进行Binder获取到(即添加到dev/binder域管理)publishBinderService(Context.INPUT_METHOD_SERVICE, mService, false /*allowIsolated*/,DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);}.......}public class InputMethodManagerService extends IInputMethodManager.Stubimplements ServiceConnection, Handler.Callback {....//IMMS构造函数public InputMethodManagerService(Context context) {mIPackageManager = AppGlobals.getPackageManager();mContext = context;mRes = context.getResources();mHandler = new Handler(this);// Note: SettingsObserver doesn't register observers in its constructor.// SettingsObserver类型,用于监听来自设置的输入法配置, 比如默认输入法, 启用的输入法, 选择的输入法等mSettingsObserver = new SettingsObserver(mHandler);mIWindowManager = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);mImeDisplayValidator = displayId -> mWindowManagerInternal.getDisplayImePolicy(displayId);.....// 状态栏输入法图标名称, 会根据这个名称设置输入法的图标显示mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);mIsLowRam = ActivityManager.isLowRamDeviceStatic();// 切换输入法时的通知Bundle extras = new Bundle();extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);.....//获取UIDint userId = 0;try {userId = ActivityManager.getService().getCurrentUser().id;} catch (RemoteException e) {Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);}// 最近切换的UIDmLastSwitchUserId = userId;//应在buildInputMethodListLocked之前创建mSettings//类型InputMethodSettings,输入法设置对象mSettings = new InputMethodSettings(mRes, context.getContentResolver(), mMethodMap, userId, !mSystemReady);updateCurrentProfileIds();AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);mMenuController = new InputMethodMenuController(this);}......}
IMMS.java中几个重要的变量:
String mCurMethodId
:系统当前默认的输入法id, 可能为空, 与Settings.Secure#DEFAULT_INPUT_METHOD
值保持一致,在setInputMethodLocked中赋值String mCurId
:当前已经绑定的输入法id, 如果没有输入法绑定上的话, 值为nullClientState mCurClient
:用于当前激活的IME, 只有持有这个令牌的IME才被系统认可IInputMethod mCurMethod
:当前已经绑定的输入法接口, 如果为null, 说明没有任何输入法连接上
2.2.4. 序列图
2.3. IME服务端(IMS)初始化流程
IMS运行在输入法进程, 是一种特殊的输入法后台服务,继承结构为:
InputMethodService extends AbstractInputMethodServiceService
输入法服务本质上是一个服务, 使用时需要IMMS通过bindService
的方式绑定。 初始化过程在Service的onCreate
方法中, 绑定方法在onBind
方法中。
涉及代码文件路径: frameworks/base/core/java/android/view/inputmethod/InputMethodManager.java frameworks/base/core/java/com/android/internal/view/IInputMethodManager.aidl frameworks/base/core/java/android/inputmethodservice/InputMethodService.java packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java
2.3.1. 函数流程梳理
# 我们从InputMethodManager.java开始梳理
InputMethodManager.java -- startInput---》 startInputInner---> IInputMethodManager.aidl -- startInputOrWindowGainedFocus---> InputMethodManagerService.java -- startInputOrWindowGainedFocus---》 startInputOrWindowGainedFocusInternal---》 startInputOrWindowGainedFocusInternalLocked---》 startInputUncheckedLocked---》 调用bindCurrentInputMethodServiceLocked,启动当前默认的输入法的服务(启动在设置的输入法进程中)---》 调用bindServiceAsUser,例如AOSP提供的packages/inputmethods/LatinIME输入法,可以在AndroidManifest.xml看到输入法service,即此处的绑定的服务# 参考packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java---》 输入法APP中会调用InputMethodManager的setAdditionalInputMethodSubtypes,然后调用到InputMethodManagerService.java对应函数---》 调用buildInputMethodListLocked检查当前默认的输入法(LatinIME)服务是否存在---》 调用setInputMethodEnabledLocked检查默认的LatinIME是否是可用的可用的输入法,如果不可用,则设置为可用(即检查settingprovider数据库的enabled_input_methods信息)# 输入法应用会继承InputMethodService,比如packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/LatinIME.java
# 因此会实现InputMethodService的onCreate函数# 从onTouchEvent开始,viewClicked流程
1 viewClicked
2 checkFocus
3 startInputInner
4 startInputOrWindowGainedFocus
5 startInputLocked
6 startInputUncheckedLocked
7 attachNewInputLocked
3. 输入法弹出流程
3.1. 点击弹出输入法流程
# 点击界面输入框应用,然后弹出输入法,在点击onTouchEvent事件后
# 一般应用会继承TextView
frameworks/base/core/java/android/widget/TextView.java -- onTouchEvent---》 viewClicked(imm) 入参InputMethodManager对象,如果IMM对象不为空,则会调用IMM的同名函数# 此处会判断服务的view是否相同,当两次点击在同一个输入框时,两者相同;否则不同
# 此处以首次点击某个输入框为例
---》 InputMethodManager.java -- 调用viewClicked(View view)---》 调用checkFocus()检查焦点,然后调用ImeFocusController的同名函数 controller.checkFocus(false /* forceNewFocus */, true /* startInput */)# 具体调用代码immDelegate.startInput(StartInputReason.CHECK_FOCUS, null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */)# 入参说明:# -startInputReason:START_INPUT_REASON_CHECK_FOCUS,标明本次调用的目的# -windowGainingFocus:会影响IMMS中startInputOrWindowGainedFocus的调用逻辑# -controlFlags,softInputMode,windowFlags = 0---》 ImeFocusController.java -- checkFocus创建获取接口类InputMethodManagerDelegate对象,其实就是IMM.java的内部类DelegateImpl(实现该接口类)对象,然后调用该类的startInput函数---》 InputMethodManager.java -- 调用类DelegateImpl的startInput函数---》 调用startInputInner函数(1)---》 创建new IInputConnectionWrapper对象(2)---》 调用IInputMethodManager.aidl的startInputOrWindowGainedFocus函数---》 InputMethodManagerService.java -- 跨进程调用startInputOrWindowGainedFocus---》 调用startInputOrWindowGainedFocusInternal---》 调用startInputOrWindowGainedFocusInternalLocked---》 调用startInputUncheckedLocked# NOTE:此处返回结果给上层InputMethodManager.java的startInputOrWindowGainedFocus函数---》 调用attachNewInputLocked函数,发送MSG_START_INPUT消息,触发handleMessage,调用startInput (---该函数返回对象InputBindResult,输入法数据结果信息---)代码说明:
# session是SessionState静态类,method是IInputMethod对象
session.method.startInput(startInputToken, inputContext, missingMethods, editorInfo, restarting);---》 IInputMethod.aidl -- startInput
---》 IInputMethodWrapper.java实现IInputMethod -- startInput,发送DO_START_INPUT消息,然后在executeMessage中调用InputMethod的dispatchStartInputWithToken# 抽象类AbstractInputMethodImpl实现了InputMethod接口类
# 静态类InputMethodSessionCallbackWrapper实现了InputMethod.SessionCallback类,主要实现sessionCreated建立session连接
---》 InputMethod.java 接口类InputMethod方法dispatchStartInputWithToken,然后调用startInput(如果是重启则调用restartInput)# InputMethodService.java中的类InputMethodImpl继承了抽象类AbstractInputMethodImpl
---》 InputMethodService.java -- 调用类InputMethodImpl的startInput方法---》 调用doStartInput# 调用以通知输入方法文本输入已在编辑器中开始。您应该使用此回调来初始化输入的状态,以匹配给定给它的编辑器的状态# 此处由具体输入法APP继承InputMethodService类然后来实现# NOTE:比如packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/LatinIME.java# 入参attibute:开始输入的编辑器的属性# 入参restarting:如果输入在同一编辑器中重新启动,例如因为应用程序更改了编辑器中的文本,则设置为true。否则将为false,表示这是一个带有编辑的新会话---》 调用onStartInput(EditorInfo attribute, boolean restarting)
3.1.1. 序列图
3.2. 输入法显示流程(隐藏)
梳理WMS部分流程。 可参考:Android输入法弹出流程
# 点击界面输入框应用,然后弹出输入法,在点击onTouchEvent事件后
# 一般应用会继承TextView
frameworks/base/core/java/android/widget/TextView.java -- onTouchEvent---》 创建InputMethodManager对象,调用showSoftInput(先执行上面点击弹出输入法viewClicked的流程,紧接着执行此处流程)----》 InputMethodManager.java --- showSoftInput(hideSoftInput隐藏输入法)----》 IInputMethodManager.aidl 跨进程showSoftInput----》IMMS.java extends IInputMethodManager.Stub 调用showSoftInput---》 showCurrentInputLocked 发送MSG_SHOW_SOFT_INPUT消息,然后在handleMessage调用IInputMethod的showSoftInput---》 IInputMethod.aidl 跨进程showSoftInput---》 IInputMethodWrapper extends IInputMethod.Stub 调用showSoftInput,发送DO_SHOW_SOFT_INPUT消息,然后在executeMessage调用InputMethod的showSoftInputWithToken函数# 抽象类AbstractInputMethodImpl实现了InputMethod接口类
# 静态类InputMethodSessionCallbackWrapper实现了InputMethod.SessionCallback类,主要实现sessionCreated建立session连接
---》 InputMethod.java 接口类InputMethod方法showSoftInputWithToke,调用showSoftInput# InputMethodService.java中的类InputMethodImpl继承了抽象类AbstractInputMethodImpl
---》 InputMethodService.java -- 调用类InputMethodImpl的方法showSoftInput,显示输入法(hideSoftInput隐藏输入法)(1)----》 dispatchOnShowInputRequested 调用onShowInputRequested(2)----》 showWindow()---》 调用startViews(prepareWindow(showInput))# prepareWindow函数调用(1)---》 initialize()(2)---》 updateFullscreenMode()(3)---》 updateInputViewShown(() 先调用onCreateInputView,然后调用setInputView(4)---》 如果mViewsCreated未创建,即为false,调用initialize(),然后调用onCreateCandidatesView(),再调用setCandidatesView(v)# startViews函数(1)---》 onStartInputView(2)---》 onStartCandidatesView(3)---》 startExtractingText(3)----》 applyVisibilityInInsetsConsumerIfNecessary(boolean setVisible) 如果条件满足,申请显示输入法-----》 InputMethodManagerService.java --- applyImeVisibilityAsync通过Binder机制调用IMMS类的applyImeVisibility函数,使申请IME可见# 隐藏输入法函数是hideIme
-----》 WMS.java --- showImePostLayout(IBinder imeTargetWindowToken)
# NOTE:该问题代码修改的地方,在Android 13上已修复
(1)-----》 windowState.java --- `InsetsControlTarget controlTarget = getImeControlTarget` 此处会获取displaycontent,还有他的parentWindow() ======》 调用 DisplayContent.java --- InsetsControlTarget getImeHostOrFallback(WindowState target)
(2)-----》 ImeInsetsSourceProvider.java --- scheduleShowImePostLayout(controlTarget),该函数主要通过Target的值处理输入法显示逻辑,如果显示则调用setImeShowing将mImeShowing全局变量设置为true,然后外部通过isImeShowing()调用获取该值-----》 checkShowImePostLayout() 判断是否显示输入法,有多个逻辑判断,包含(1) mWin != null (即windowtoken - imeTargetWindowToken)(2) mWin.isDrawn()(3) !mWin.mGivenInsetsPending(4) mIsImeLayoutDrawn = true(5) 调用isReadyToShowIme()函数,判断两个target的内容(目标窗口的target和请求的target)----》 setImeShowing(true) 设置输入法可显示 (和isImeShowing()成对,用于外部获取)
3.2.1. 序列图
4. 输入法常见问题小结
4.1. 配置默认输入法
4.1.1. 查看已安装的输入法
adb shell ime list -a# 结果
com.android.inputmethod.latin/.CarLatinIME:# mId就是配置项def_input_methodmId=com.android.inputmethod.latin/.CarLatinIME mSettingsActivityName=null mIsVrOnly=false mSupportsSwitchingToNextInputMethod=false mInlineSuggestionsEnabled=false mSuppressesSpellChecker=false mShowInInputMethodPicker=truemIsDefaultResId=0x0Service:priority=0 preferredOrder=0 match=0x108000 specificIndex=-1 isDefault=falseServiceInfo:# 输入法服务名name=com.android.inputmethod.latin.CarLatinIME# 输入法包名packageName=com.android.inputmethod.latinlabelRes=0x7f08000c nonLocalizedLabel=null icon=0x0 banner=0x0enabled=true exported=true directBootAware=true# 权限permission=android.permission.BIND_INPUT_METHODflags=0x0ApplicationInfo:packageName=com.android.inputmethod.latinlabelRes=0x7f08000c nonLocalizedLabel=null icon=0x7f04000c banner=0x0processName=com.android.inputmethod.latintaskAffinity=com.android.inputmethod.latinuid=10058 flags=0x38c8be45 privateFlags=0xa4400040 theme=0x103013erequiresSmallestWidthDp=0 compatibleWidthLimitDp=0 largestWidthLimitDp=0# APK目录sourceDir=/system/app/CarLatinIME/CarLatinIME.apkseinfo=default:targetSdkVersion=23seinfoUser=:complete# data数据目录dataDir=/data/user/0/com.android.inputmethod.latindeviceProtectedDataDir=/data/user_de/0/com.android.inputmethod.latincredentialProtectedDataDir=/data/user/0/com.android.inputmethod.latinsharedLibraryFiles=[/system/framework/android.test.base.jar, /system/framework/android.hidl.manager-V1.0-java.jar, /system/framework/android.hidl.base-V1.0-java.jar, /system/framework/org.apache.http.legacy.jar]enabled=true minSdkVersion=32 targetSdkVersion=23 versionCode=32 targetSandboxVersion=1supportsRtl=truefullBackupContent=truecrossProfile=falseHiddenApiEnforcementPolicy=0usesNonSdkApi=trueallowsPlaybackCapture=falsenativeHeapZeroInitialized=0
4.1.2. 切换输入法
adb shell ime set com.android.inputmethod.latin/.CarLatinIME
4.1.3. Android代码配置新默认输入法
Android默认输入法的配置和两个关键词相关,都保存在SettingsProvider或者settings_secure.xml里面
- enabled_input_methods:表示配置的系统允许使用的输入法的id字符串,中间以冒号分隔,比如com.android.inputmethod.latin/.LatinIME:com.xxxx.inputmethod.remote/.RemoteIME
- default_input_method:表示配置的系统默认使用的输入法
在InputMethodService启动时,一方面会从packagemanager那边获取InputMethodService的应用信息,另一方面也会settings数据库里面读取enabled_input_methods
和default_input_method
对应的输入法信息。
- 如果后者是空的,则会把前者保存起来并enable,并通过
InputMethodUtils.getMostApplicableDefaultIME
方法来获取最适合当前系统的输入法,并设置为默认的输入法 - 如果后者不是空的,则会读取settings数据库的default_input_method信息,如果是有效的输入法就会把它设置为默认的输入法,如果不是有效的是空的则还是会通过
getMostApplicableDefaultIME
方法来获取最适合当前系统的输入法
//frameworks/base/core/java/android/provider/Settings.java/*** List of input methods that are currently enabled. This is a string* containing the IDs of all enabled input methods, each ID separated* by ':'.** Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"* where imeId is ComponentName and subtype is int32.*/@Readablepublic static final String ENABLED_INPUT_METHODS = "enabled_input_methods";/*** Setting to record the input method used by default, holding the ID* of the desired method.*/@Readablepublic static final String DEFAULT_INPUT_METHOD = "default_input_method";
配置修改方法:
//frameworks/base/packages/SettingsProvider/res/values/defaults.xml
//包名+输入法 ID(服务名), 可通过ime list -s查看
+ <!-- set default input method-->
+ <string name="def_input_method" translatable="false">com.test.inputmethod.pinyin/.PinyinIME</string>
+ <string name="enabled_input_methods" translatable="false">com.test.inputmethod.pinyin/.PinyinIME:com.android.inputmethod.pinyin/.PinyinIME</string>//frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java+ loadStringSetting(stmt, Settings.Secure.DEFAULT_INPUT_METHOD,
+ R.string.def_input_method);
+ loadStringSetting(stmt, Settings.Secure.ENABLED_INPUT_METHODS,
+ R.string.enabled_input_methods);
其中配置项的字符串获取方式是:
- 安装上apk,在settings界面选中该输入法(或者命令ime set),然后命令行执行:
settings get secure def_input_method
和settings get secure enabled_input_method
- 通过
ime list -s
查看输入法ID的信息,然后修改此处配置项
4.2. Android 12输入法无法在虚拟屏显示
Android 12移除了Android 11上portalToDisplayId的部分代码,导致Android 12上创建Virtual Display后,无法在虚拟屏弹出输入法。回退代码如下:
备注:android-12.0.0_r3仍未移除,可在AOSP源码上查看到
core/jni/android_hardware_input_InputWindowHandle.cpp
:添加portalToDisplayId,通过JNI获取JVM层portalToDisplayId的值和FIELD_ID
+ jfieldID portalToDisplayId;+ mInfo.portalToDisplayId = env->GetIntField(obj,
+ gInputWindowHandleClassInfo.portalToDisplayId);+ GET_FIELD_ID(gInputWindowHandleClassInfo.portalToDisplayId, clazz,
+ "portalToDisplayId", "I");
services/surfaceflinger/Layer.cpp
:在fillInputInfo函数中,由于当前虚拟屏Touch实现的方案,getLayerStack会返回0,会覆盖掉WMS传递给SF的DisplayID的值。由于这里会更新InputHandleInfo,传递给InputManager,如果DisplayID的值不正确,会影响InputManager内部关于Focus的计算逻辑
WindowInfo Layer::fillInputInfo(const sp<DisplayDevice>& display) {if (!hasInputInfo()) {mDrawingState.inputInfo.name = getName();mDrawingState.inputInfo.ownerUid = mOwnerUid;mDrawingState.inputInfo.ownerPid = mOwnerPid;mDrawingState.inputInfo.inputFeatures = WindowInfo::Feature::NO_INPUT_CHANNEL;mDrawingState.inputInfo.flags = WindowInfo::Flag::NOT_TOUCH_MODAL;mDrawingState.inputInfo.displayId = getLayerStack();}WindowInfo info = mDrawingState.inputInfo;info.id = sequence;+ if (info.displayId == ADISPLAY_ID_NONE) {info.displayId = getLayerStack();
+ }......
}
流程如下,从SurfaceFlinger开始合成流程开始:
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp -- onMessageReceived开始触发合成---> onMessageInvalidate---> updateInputFlinger()更新InputFlinger---> updateInputWindowInfo()更新input窗口信息---> frameworks/native/services/surfaceflinger/Layer.cpp -- fillInputInfo(DisplayDevice display) 入参display信息// 代码内容
InputWindowInfo Layer::fillInputInfo(const sp<DisplayDevice>& display) {//绘制的inputinfo不为空,mDrawingState.inputInfo.tokenif (!hasInputInfo()) {mDrawingState.inputInfo.name = getName();mDrawingState.inputInfo.ownerUid = mOwnerUid;mDrawingState.inputInfo.ownerPid = mOwnerPid;mDrawingState.inputInfo.inputFeatures = InputWindowInfo::Feature::NO_INPUT_CHANNEL;mDrawingState.inputInfo.flags = InputWindowInfo::Flag::NOT_TOUCH_MODAL;mDrawingState.inputInfo.displayId = getLayerStack();}InputWindowInfo info = mDrawingState.inputInfo;info.id = sequence;//此处注意输入窗口的display idif (info.displayId == ADISPLAY_ID_NONE) {info.displayId = getLayerStack();}// Transform that goes from "logical(rotated)" display to physical/unrotated display.// This is for when inputflinger operates in physical display-space.ui::Transform toPhysicalDisplay;if (display) {toPhysicalDisplay = display->getTransform();info.displayWidth = display->getWidth();info.displayHeight = display->getHeight();}fillInputFrameInfo(info, toPhysicalDisplay);.....
}---> fillInputFrameInfo(info, toPhysicalDisplay)计算input窗口显示合成区域
4.3. Android 12输入法无法显示在虚拟屏内部
修改方法: packages/SettingsProvider/res/values/defaults.xml
和packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java
:开启强制桌面模式,可以解决输入法显示在虚拟屏内部
- 通过
settings get global force_desktop_mode_on_external_displays
获取该值(默认是null,一般是通过开发者选项中进行开启/关闭) - 手动命令设置设置桌面模式:
settings put global force_desktop_mode_on_external_displays 1
//defaults.xml
+ <!-- Initial value for the Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS -->
+ <integer name="def_force_desktop_mode_on_external_displays">1</integer>//DatabaseHelper.java
+ // set global value of DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+ loadIntegerSetting(stmt, Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+
4.3.1. 开发者选项桌面模式
开发者选项中桌面模式的开关,主要就是DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS值的控制
//packages/apps/Settings/src/com/android/settings/development/DesktopModePreferenceController.java
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
public class DesktopModePreferenceController extends DeveloperOptionsPreferenceControllerimplements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {.......@Overridepublic boolean onPreferenceChange(Preference preference, Object newValue) {final boolean isEnabled = (Boolean) newValue;Settings.Global.putInt(mContext.getContentResolver(),DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);return true;}@Overridepublic void updateState(Preference preference) {final int mode = Settings.Global.getInt(mContext.getContentResolver(),DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, SETTING_VALUE_OFF);((SwitchPreference) mPreference).setChecked(mode != SETTING_VALUE_OFF);}@Overrideprotected void onDeveloperOptionsSwitchDisabled() {super.onDeveloperOptionsSwitchDisabled();Settings.Global.putInt(mContext.getContentResolver(),DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, SETTING_VALUE_OFF);((SwitchPreference) mPreference).setChecked(false);}
}
4.3.2. 桌面模式属性流程
4.3.2.1. 修改方法
在WMS.java中获取值,并监听变化,赋值给变量mForceDesktopModeOnExternalDisplays
//frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;public class WindowManagerService extends IWindowManager.Stubimplements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {......//强制桌面模式标志//通过监听updateForceDesktopModeOnExternalDisplays()boolean mForceDesktopModeOnExternalDisplays;//构造函数时获取该值mForceDesktopModeOnExternalDisplays = Settings.Global.getInt(resolver,DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;//通过监听设置应用上开关的变化,然后在onChange调用updateForceDesktopModeOnExternalDisplays()函数更新mForceDesktopModeOnExternalDisplays值private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor(Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS);
4.3.2.2. mForceDesktopModeOnExternalDisplays值流程
第一处:
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp -- setDisplayViewports输入管理服务的函数,用来设置输入系统需要的显示器的显示视图信息
---> JNI函数getPointerDisplayId()调用InputManagerService的对应函数frameworks/base/services/core/java/com/android/server/input/InputManagerService.java -- getPointerDisplayId()---> frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java -- getPointerDisplayId()如果桌面模式打开则displayid=DEFAULT_DISPLAY,否则从WMS对象的displaycontent中获取displayId
第二处:
DisplayContent.java -- 此处三个地方会调用到updateImeControlTarget函数:
(1)setImeLayeringTargetInner(@Nullable WindowState target)由computeImeTarget(boolean updateImeTarget)计算IME Target的函数调用
(2)updateImeInputAndControlTarget(WindowState target)由WindowState.java或者WMS.java中调用
(3)setInputMethodWindowLocked(WindowState win)由RootWindowContainer.java或者WMS.java调用---> updateImeControlTarget()
---> computeImeControlTarget()
---> getImeHostOrFallback(WindowState target)
---> getImePolicy()
---> forceDesktopMode()判断是否开启桌面模式 && 非defalut display && 非private的Flag,返回布尔值
4.4. Android 12输入法无法在多个虚拟屏切换显示
4.4.1. 修改方法
参考:Google官方文档-屏幕支持
overlay/packages/services/Car/car_product/overlay/frameworks/base/core/res/res/values/config.xml
:关闭多屏焦点,为了解决输入法无法在多个虚拟屏切换显示的问题
该配置项阅读官方文档,意思就是为了同时支持多个以单个屏幕为目标的输入源,可以将Android配置为支持多个聚焦窗口(每个屏幕最多支持一个)。
由Android 10引入,在Android 9即更低版本中,系统中一次最多只有一个窗口具有焦点。
然而在多个Virtual Display虚拟屏中,开启该配置项,输入法只会将焦点聚焦在一个虚拟屏中,无法切换虚拟屏点击弹出输入法,所以关闭该配置项。
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">+ <!-- disable multi display focus -->
+ <!-- Whether the system enables per-display focus. If the system has the input method for each
+ display, this value should be true. -->
+ <bool name="config_perDisplayFocusEnabled">false</bool>
</resources>
4.4.2. 多屏焦点流程
- InputDispatcher现在可以有多个聚焦窗口(每个屏幕一个)。如果某个输入事件特定于屏幕,则该事件会被分派到相应屏幕中的聚焦窗口。否则,它会被分派到聚焦屏幕(即用户最近与之交互的屏幕)中的聚焦窗口。参阅 InputDispatcher::setFocusedDisplay()。聚焦应用也会通过NativeInputManager::setFocusedApplication()在InputManagerService中分别更新
- 在WindowManager中,系统还会单独跟踪聚焦窗口。参阅DisplayContent#mCurrentFocus和DisplayContent#mFocusedApp以及各自的用途。相关的焦点跟踪和更新方法已从WindowManagerService移至DisplayContent
以下是WMS焦点更新时,多屏焦点的流程:
WindowManagerService.java构造函数中获取config_perDisplayFocusEnabled属性值,将其赋值给全局变量mPerDisplayFocusEnabled,用于控制此功能的可用性InsetsSourceProvider.java -- setWindow更新当前支持此源的窗口,此处只关注IME输入法
---> updateControlForTarget更新Target
---> setClientVisible(boolean clientVisible)设置Client端可见,发送LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED消息给WMS---> WMS.java -- handleMessage处理LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED消息,调用layoutAndAssignWindowLayersIfNeeded函数---> DisplayContent.java -- layoutAndAssignWindowLayersIfNeeded() 每当对层次结构进行视觉更改(如移动容器或调整容器大小)时,都可能调用此方法
---> updateFocusedWindowLocked更新焦点窗口,此处会通过返回的newFocus和mCurrentFocus比较
---> findFocusedWindowIfNeeded(int topFocusedDisplayId) 判断多屏焦点属性mPerDisplayFocusEnabled的值,如果开启则在该显示器上查询聚焦窗口,否则返回NULL
4.4.3. PS:多屏异显
PS:多屏异显(物理屏)的修改是将下面的属性改成false
<!-- When true, local displays that do not contain any of their own content will automaticallymirror the content of the default display. --><bool name="config_localDisplaysMirrorContent">false</bool>
4.5. Android 12输入法未显示在对应DisplayID上面
4.5.1. 修改方法
services/core/java/com/android/server/wm/WindowState.java
:修改getImeControlTarget的返回值,Android 13上此处已修改。修复IME不显示在对应的display id上。
通过dumpsys window windows查看:显示的display id(是正常实际点击到的虚拟屏display id)(未修改时会一直指向display id=0):
imeLayeringTarget in display# 3 Window{d8d67ca u0 com.example.test/com.example.test.MainActivity}
InsetsControlTarget getImeControlTarget() {final DisplayContent dc = getDisplayContent();final WindowState parentWindow = dc.getParentWindow();+ // If target's display has a parent, IME is displayed in the parent display.
+ // return dc.getImeHostOrFallback(parentWindow != null ? parentWindow : this);
+ // fix ime show on current virtual display
+ // use dump window to see imeLayeringTarget in correct display
+ return dc.getImeHostOrFallback(this);}
5. 调试技巧
5.1. adb shell ime
# 列出所有输入法服务
adb shell ime list -a -s
# 设置输入法
adb shell ime set com.sohu.inputmethod.sogouoem/.SogouIME
# 启用输入法
adb shell ime enable com.sohu.inputmethod.sogouoem/.SogouIME
# 不启用输入法
adb shell ime disable com.sohu.inputmethod.sogouoem/.SogouIME
# 重置为默认输入法
adb shell ime reset
# 从设置获取默认输入法
adb shell settings get secure default_input_method
5.2. Dump获取信息
5.2.1. 获取到输入法的各种信息
adb shell dumpsys input_method# 可以获取到输入法的各种信息, 并且可以过滤window信息
adb shell dumpsys input_method | grep -i window
5.2.2. 获取输入法的窗口状态信息
# 获取输入法的窗口状态信息
adb shell dumpsys window | grep -i input
5.2.3. 获取输入法的窗口层级信息
# 获取输入法的窗口层级信息
adb shell dumpsys SurfaceFlinger
5.2.4. 获取当前存在几个isplay
通过dumpsys display查看当前有几个display:
adb shell dumpsys displayDisplay States: size=3Display Id=0Display State=ONDisplay Brightness=0.05Display SdrBrightness=0.05Display Id=2Display State=ONDisplay Brightness=0.0Display SdrBrightness=0.0Display Id=3Display State=ONDisplay Brightness=0.0Display SdrBrightness=0.0
5.2.5. 通过adb命令将app启动在指定的屏幕
# 命令参考
adb shell am start -n com.android.demo/com.android.demo.MainActivity --display 1
adb shell am start -n com.android.demo/com.android.demo.MainActivity --user 0 --display 1# Android 12原生setting
adb shell
# 原生设置界面
am start -n com.android.car.settings/.Settings_Launcher_Homepage --display 2
- 参数–display指定屏幕, display 0,表示第一块屏幕; display 1,表示第2块屏幕
- 参数–user可以启动指定的用户,在多用户下有效,系统默认是–user 0
5.3. 应用使用输入法
5.3.1. 获取输入法打开的状态
//获取输入法打开的状态
public static boolean isShowing(Context context) {InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);return imm.isActive();//isOpen若返回true,则表示输入法打开
}
5.3.2. 调用显示系统默认的输入法
- 方法一:
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(m_receiverView /*接受软键盘输入的视图(View)*/, InputMethodManager.SHOW_FORCED /*提供当前操作的标记,SHOW_FORCED表示强制显示*/);//view为接受软键盘输入的视图,SHOW_FORCED表示强制显示
public static void showOrHide(Context context, View view) {InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);//imm.showSoftInput(view, InputMethodManager.SHOW_FORCED); //SHOW_FORCED表示强制显示//imm.hideSoftInputFromWindow(view.getWindowToken(), 0); //强制隐藏键盘
}
- 方法二:
//如果输入法在窗口上已经显示,则隐藏,反之则显示
public static void showOrHide(Context context) {InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);//这个方法可以实现输入法在窗口上切换显示,如果输入法在窗口上已经显示,则隐藏;如果隐藏,则显示输入法到窗口上//该方法在IMM中最终会调用hideSoftInputFromWindow或者showSoftInputimm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
}
5.3.3. 调用隐藏系统默认的输入法
//调用隐藏系统默认的输入法
public static void showOrHide(Context context, Activity activity) {//activity为当前activity((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(),InputMethodManager.HIDE_NOT_ALWAYS);
}