Android IMS框架流程

模块简图:

输入法配置:

1.1 控制可用输入法和默认输入法:

文件:frameworks/base/packages/SettingsProvider/res/values/defaults.xml
字段:<string name="enabled_input_methods" translatable="false">com.sogou.inputmethod.iot/com.sogou.iot.SogouInputMethodService:com.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME</string>
<string name="default_input_method" translatable="false">com.sogou.inputmethod.iot/com.sogou.iot.SogouInputMethodService</string>

代码流程:

2.1 客户端应用注册到输入法系统服务

2.1.1 创建LocationManger

1)客户端应用创建时,调用ViewRoot(…);
2)调用ViewRoot .getWindowSession(…);
3)通过调用InputMethodManager.getInstance()创建LocationManager对象,一个客户端应用只会创建一个
LocationManager对象;
4)LocationManager对象创建时,会创建一个IInputMethodClient对象,同时创建一个IInputContext对象;

2.1.2 注册到输入法系统服务

1)调用WindowMangerService. openSession(…);
2)调用WindowMangerService. Session(…);
3)调用InputMethodMangerService.addClient(IInputMethodClientclient, IInputContext inputContext, int uid, int
pid),将InputMethodManager中创建的IInputMethodClient对象以及InputMethodManager中创建的IInputContext
对象传入进去,其中uid为客户端应用用户ID,pid为客户端应用进程ID;
4)加入到InputMethodManagerService维护的一个列表HashMap<IBinder, ClientState>中,其中IBinder对应
IInputMethodClient,一个InputMethodManager只有一个IInputMethodCliend。
 

2.2 客户端应用发起调用输入法的请求

2.2.1 按下输入框触发焦点请求

1)TextView.performAccessibilityActionClick(); //按下输入框,触发点击
2) View.requestFocus();//请求获取焦点

2.2.2 判断是否需要显示输入法

1)调用InputMethodManager. startInput() ——> 调用InputMethodManager. startInputInner();
//InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
            @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags) {
        ...

        if (windowGainingFocus == null) {
            windowGainingFocus = view.getWindowToken();
            if (windowGainingFocus == null) {
                Log.e(TAG, "ABORT input: ServedView must be attached to a Window");
                return false;
            }
            startInputFlags = getStartInputFlags(view, startInputFlags);
            softInputMode = view.getViewRootImpl().mWindowAttributes.softInputMode;
            windowFlags = view.getViewRootImpl().mWindowAttributes.flags;
        }

        ...

        // Okay we are now ready to call into the served view and have it
        // do its stuff.
        // Life is good: let's hook everything up!
        EditorInfo tba = new EditorInfo();
        // Note: Use Context#getOpPackageName() rather than Context#getPackageName() so that the
        // system can verify the consistency between the uid of this process and package name passed
        // from here. See comment of Context#getOpPackageName() for details.
        tba.packageName = view.getContext().getOpPackageName();
        tba.autofillId = view.getAutofillId();
        tba.fieldId = view.getId();
        InputConnection ic = view.onCreateInputConnection(tba);
        if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);

        synchronized (mH) {
            // Now that we are locked again, validate that our state hasn't
            // changed.
            final View servedView = getServedViewLocked();
            if (servedView != view || !mServedConnecting) {
                // Something else happened, so abort.
                if (DEBUG) Log.v(TAG,
                        "Starting input: finished by someone else. view=" + dumpViewInfo(view)
                        + " servedView=" + dumpViewInfo(servedView)
                        + " mServedConnecting=" + mServedConnecting);
                return false;
            }

            // If we already have a text box, then this view is already
            // connected so we want to restart it.
            if (mCurrentTextBoxAttribute == null) {
                startInputFlags |= StartInputFlags.INITIAL_CONNECTION;
            }

            // Hook 'em up and let 'er rip.
            mCurrentTextBoxAttribute = tba;
            maybeCallServedViewChangedLocked(tba);
            mServedConnecting = false;
            if (mServedInputConnectionWrapper != null) {
                mServedInputConnectionWrapper.deactivate();
                mServedInputConnectionWrapper = null;
            }
            ControlledInputConnectionWrapper servedContext;
            final int missingMethodFlags;
            if (ic != null) {
                mCursorSelStart = tba.initialSelStart;
                mCursorSelEnd = tba.initialSelEnd;
                mCursorCandStart = -1;
                mCursorCandEnd = -1;
                mCursorRect.setEmpty();
                mCursorAnchorInfo = null;
                final Handler icHandler;
                missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
                if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
                        != 0) {
                    // InputConnection#getHandler() is not implemented.
                    icHandler = null;
                } else {
                    icHandler = ic.getHandler();
                }
                servedContext = new ControlledInputConnectionWrapper(
                        icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
            } else {
                servedContext = null;
                missingMethodFlags = 0;
            }
            mServedInputConnectionWrapper = servedContext;

            try {
                if (DEBUG) Log.v(TAG, "START INPUT: view=" + dumpViewInfo(view) + " ic="
                        + ic + " tba=" + tba + " startInputFlags="
                        + InputMethodDebug.startInputFlagsToString(startInputFlags));
                final InputBindResult res = mService.startInputOrWindowGainedFocus(
                        startInputReason, mClient, windowGainingFocus, startInputFlags,
                        softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
                        view.getContext().getApplicationInfo().targetSdkVersion);
                 ...
            } catch (RemoteException e) {
                Log.w(TAG, "IME died: " + mCurId, e);
            }
        }

        return true;
    1. startInputFlags = getStartInputFlags(view, startInputFlags); ,这行代码对获取焦点的View的类型做了判断,如果View是一个TextEditor,则startInputFlags会增加一个StartInputFlags.IS_TEXT_EDITOR的标志,这个标志十分重要,它决定了IME的显示与隐藏的行为。
    2. 在创建完InputConnection后,调用了IMMS的startInputOrWindowGainedFocus方法,控制IME App的显示或隐藏。其函数内(startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) 这段代码是非EditText获取到焦点无法弹出IME的关键逻辑。

2.2.3 完成系统IMMS与应用IME的绑定

1)InputMethodManagerService.startInputUncheckedLocked
//InputMethodManagerService.java
 InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
            @MissingMethodFlags int missingMethods, @NonNull EditorInfo attribute,
            @StartInputFlags int startInputFlags, @StartInputReason int startInputReason) {
        ...

        mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
        mCurIntent.setComponent(info.getComponent());
        mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
                com.android.internal.R.string.input_method_binding_label);
        mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
                mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
                PendingIntent.FLAG_IMMUTABLE));

        if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
            mLastBindTime = SystemClock.uptimeMillis();
            mHaveConnection = true;
            mCurId = info.getId();
            mCurToken = new Binder();
            mCurTokenDisplayId = displayIdToShowIme;
            try {
                if (DEBUG) {
                    Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
                            + mCurTokenDisplayId);
                }
                mIWindowManager.addWindowToken(mCurToken, LayoutParams.TYPE_INPUT_METHOD,
                        mCurTokenDisplayId);
            } catch (RemoteException e) {
            }
            return new InputBindResult(
                    InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
                    null, null, mCurId, mCurSeq, null);
        }
        mCurIntent = null;
        Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
        return InputBindResult.IME_NOT_CONNECTED;
    }
上述代码将IMMS 与实际输入法服务建立了绑定关系,Intent的具体参数如下:
    • Action: InputMethod.SERVICE_INTERFACE: "android.view.InputMethod"
    • Component:默认输入法App的Component信息,它是动态查询的;当系统存在多个输入法App时,由Settings.Secure.DEFAULT_INPUT_METHOD属性决定默认的输入法。
    • Extra: EXTRA_CLIENT_LABEL,一个label,携带的信息是不同语言下的”输入法“这个词语,不太重要。
    • Extra: EXTRA_CLIENT_INTENT,输入法设置页面Activity路径,一般配置在系统的Settings App中。
通过这一部分代码,IMMS与系统默认的IME App最终建立了绑定关系,实现了远端相互通讯。
 

2.3 客户端创建会话

2.3.1  bindService成功后,触发onServiceConnected的回调:
//InputMethodManagerService.java
public void onServiceConnected(ComponentName name, IBinder service) {
        synchronized (mMethodMap) {
            if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
              ...
                if (mCurClient != null) {
                    clearClientSessionLocked(mCurClient);
                    requestClientSessionLocked(mCurClient);
                }
            }
        }
    }
此回调中关键语句在于requestClientSessionLocked(mCurClient);,它的作用是创建一个IM客户端Session——IInputMethodSession,用于IMMS与IME(InputMethodEditor)的具体事件通讯,如软键盘字符按下的事件、隐藏软键盘等。
 
2.3.2 创建一个IM客户端Session并注册,触发MethodCallback的回调:
//InputMethodManagerService.java
 void requestClientSessionLocked(ClientState cs) {
        if (!cs.sessionRequested) {
            if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
            InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
            cs.sessionRequested = true;
            executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
                    MSG_CREATE_SESSION, mCurMethod, channels[1],
                    new MethodCallback(this, mCurMethod, channels[0])));
        }
    }
请注意上述代码的MethodCallback这个类,从命名上可以看出,它是一个回调接口。事实上,当IMMS成功创建IInputMethodSession并将它注册到服务端后,MethodCallback就会收到Session创建成功后由sessionCreated调用的onSessionCreated回调。
 

2.4 输入法系统服务调起输入法

2.4.1 应用向系统IMMS发起handler消息请求

1)InputMethodManagerService.onSessionCreated——>
InputMethodManagerService.attachNewInputLocked :
//InputMethodManagerService.java
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
        if (!mBoundToMethod) {
        ...
        executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
                MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
                startInputToken, session, mCurInputContext, mCurAttribute));
        if (mShowRequested) {
            if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
            showCurrentInputLocked(mCurFocusedWindow, getAppShowFlags(), null,
                    SoftInputShowHideReason.ATTACH_NEW_INPUT);
        }
        return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
                session.session, (session.channel != null ? session.channel.dup() : null),
                mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
    }
它向Handler发送了一条MSG_START_INPUT的消息:
//InputMethodManagerService.java
   case MSG_START_INPUT: {
                final int missingMethods = msg.arg1;
                final boolean restarting = msg.arg2 != 0;
                args = (SomeArgs) msg.obj;
                final IBinder startInputToken = (IBinder) args.arg1;
                final SessionState session = (SessionState) args.arg2;
                final IInputContext inputContext = (IInputContext) args.arg3;
                final EditorInfo editorInfo = (EditorInfo) args.arg4;
                try {
                    setEnabledSessionInMainThread(session);
                    session.method.startInput(startInputToken, inputContext, missingMethods,
                            editorInfo, restarting, session.client.shouldPreRenderIme);
                } catch (RemoteException e) {
                }
                args.recycle();
                return true;
            }
session.method.startInput这句话开始,中间会经历
  IInputMethodWrapper.startInput——>
  InputMethod.dispatchStartInputWithToken——>
  InputManagerService.startInput——>
  InputMethodService.doStartInput

2.4.2 系统IMMS将输入法窗口显示出来

1)最终调用到IMMS bind服务端——InputMethodService的doStartInput方法中:
//InputMethodService.java
 void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
        ...
        if (mDecorViewVisible) {
           ...
        } else if (mCanPreRender && mInputEditorInfo != null && mStartedInputConnection != null) {
            // Pre-render IME views and window when real EditorInfo is available.
            // pre-render IME window and keep it invisible.
            if (DEBUG) Log.v(TAG, "Pre-Render IME for " + mInputEditorInfo.fieldName);
            if (mInShowWindow) {
                Log.w(TAG, "Re-entrance in to showWindow");
                return;
            }

            mDecorViewWasVisible = mDecorViewVisible;
            mInShowWindow = true;
            startViews(prepareWindow(true /* showInput */));

            // compute visibility
            mIsPreRendered = true;
            onPreRenderedWindowVisibilityChanged(false /* setVisible */);

            // request draw for the IME surface.
            // When IME is not pre-rendered, this will actually show the IME.
            if (DEBUG) Log.v(TAG, "showWindow: draw decorView!");
            mWindow.show();
            maybeNotifyPreRendered();
            mDecorViewWasVisible = true;
            mInShowWindow = false;
        } else {
            mIsPreRendered = false;
        }
    }
doStartInput函数代码中的mWindow.show()调用标识着IME软键盘的UI界面已经成功展示在屏幕上,至此完成点击输入框到调出软键盘的整个流程。
posted @ 2024-03-28 11:05  小汀  阅读(82)  评论(0编辑  收藏  举报