CTS问题分析16

最近好像和输入法比较有缘啊,又是一个定制的输入法造成的CTS问题;话说,一般第三方app造成CTS问题的情况一般是危险权限之类的,输入法顶多是window遮挡了uiautomator待识别的控件;今天这个问题还真是以前没见过的,因此记录下这个问题

问题初探

测试命令: run cts -m CtsWidgetTestCases -t android.widget.cts.TextViewTest#testUndo_directAppend

测试case如下:

2006    @Test
2007    public void testUndo_directAppend() throws Throwable {
2008        initTextViewForTypingOnUiThread();
2009
2010        // Type some text.
2011        CtsKeyEventUtil.sendString(mInstrumentation, mTextView, "abc");
2012        mActivityRule.runOnUiThread(() -> {
2013            // Programmatically append some text.
2014            mTextView.append("def");
2015            assertEquals("abcdef", mTextView.getText().toString());
2016
2017            // Undo removes the append as a separate step.
2018            mTextView.onTextContextMenuItem(android.R.id.undo);
2019            assertEquals("abc", mTextView.getText().toString());
2020
2021            // Another undo removes the original typing.
2022            mTextView.onTextContextMenuItem(android.R.id.undo);
2023            assertEquals("", mTextView.getText().toString());
2024        });
2025        mInstrumentation.waitForIdleSync();
2026    }

fail log:

03-13 16:39:28 I/ConsoleReporter: [1/1 armeabi-v7a CtsWidgetTestCases 55dbc44c0209] android.widget.cts.TextViewTest#testUndo_directAppend fail: org.junit.ComparisonFailure: expected:<[abc]> but was:<[]>
at org.junit.Assert.assertEquals(Assert.java:115)
at org.junit.Assert.assertEquals(Assert.java:144)
at android.widget.cts.TextViewTest.lambda$-android_widget_cts_TextViewTest_83724(TextViewTest.java:2019)
at android.widget.cts.-$Lambda$sfabuQl5m69f6xjFtBWw9xUqP20.$m$588(Unknown Source:4)
at android.widget.cts.-$Lambda$sfabuQl5m69f6xjFtBWw9xUqP20.run(Unknown Source:2363)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at android.app.Instrumentation$SyncRunnable.run(Instrumentation.java:2095)
at android.os.Handler.handleCallback(Handler.java:794)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:176)
at android.app.ActivityThread.main(ActivityThread.java:6662)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)

这条case的大意是:首先模拟key down在TextView里传递abc字符串,然后对相应的TextView调用append("def") api;然后执行undo操作;预期结果是将def回退,结果变成了将整个字符串都回退了,mTextView中的Editor变成了空串,因此case fail;

问题分析

首先从undo操作开始看起

10803    /**
10804     * Called when a context menu option for the text view is selected.  Currently
10805     * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
10806     * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
10807     *
10808     * @return true if the context menu item action was performed.
10809     */
10810    public boolean onTextContextMenuItem(int id) {
10811        int min = 0;
10812        int max = mText.length();
10813
10814        if (isFocused()) {
10815            final int selStart = getSelectionStart();
10816            final int selEnd = getSelectionEnd();
10817
10818            min = Math.max(0, Math.min(selStart, selEnd));
10819            max = Math.max(0, Math.max(selStart, selEnd));
10820        }
10821
10822        switch (id) {
10823            case ID_SELECT_ALL:
10824                final boolean hadSelection = hasSelection();
10825                selectAllText();
10826                if (mEditor != null && hadSelection) {
10827                    mEditor.invalidateActionModeAsync();
10828                }
10829                return true;
10830
10831            case ID_UNDO:
10832                if (mEditor != null) {
10833                    mEditor.undo();
10834                }
10835                return true;  // Returns true even if nothing was undone.
10836
10837            case ID_REDO:
10838                if (mEditor != null) {
10839                    mEditor.redo();
10840                }
10841                return true;  // Returns true even if nothing was undone.
10842
10843            case ID_PASTE:
10844                paste(min, max, true /* withFormatting */);
10845                return true;
10846
10847            case ID_PASTE_AS_PLAIN_TEXT:
10848                paste(min, max, false /* withFormatting */);
10849                return true;
10850
10851            case ID_CUT:
10852                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
10853                deleteText_internal(min, max);
10854                return true;
10855
10856            case ID_COPY:
10857                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
10858                stopTextActionMode();
10869                return true;
10870
10871            case ID_REPLACE:
10872                if (mEditor != null) {
10873                    mEditor.replace();
10874                }
10875                return true;
10876
10877            case ID_SHARE:
10878                shareSelectedText();
10879                return true;
10880
10881            case ID_AUTOFILL:
10882                requestAutofill();
10883                stopTextActionMode();
10884                return true;
10885        }
10886        return false;
10887    }

可以看到就是执行Editor的undo操作

365    void undo() {
366        if (!mAllowUndo) {
367            return;
368        }
369        UndoOwner[] owners = { mUndoOwner };
370        mUndoManager.undo(owners, 1);  // Undo 1 action.
371    }

就是调用UndoManager的undo操作

224    /**
225     * Perform undo of last/top count undo states.  The states impacted
226     * by this can be limited through owners.
227     * @param owners Optional set of owners that should be impacted.  If null, all
228     * undo states will be visible and available for undo.  If non-null, only those
229     * states that contain one of the owners specified here will be visible.
230     * @param count Number of undo states to pop.
231     * @return Returns the number of undo states that were actually popped.
232     */
233    public int undo(UndoOwner[] owners, int count) {
234        if (mWorking != null) {
235            throw new IllegalStateException("Can't be called during an update");
236        }
237
238        int num = 0;
239        int i = -1;
240
241        mInUndo = true;
242
243        UndoState us = getTopUndo(null);
244        if (us != null) {
245            us.makeExecuted();
246        }
247
248        while (count > 0 && (i=findPrevState(mUndos, owners, i)) >= 0) {
249            UndoState state = mUndos.remove(i);
250            state.undo();
251            mRedos.add(state);
252            count--;
253            num++;
254        }
255
256        mInUndo = false;
257
258        return num;
259    }
260

可以看到UndoManager中维护一个mUndos队列,那么很自然的想到是不是这个队列出了问题,将abc和def合并了? 调一下,发现果然是,当调用undo时,其结果不正确

正常情况下应该为


editor_undo.png

但是实际上fail时undo队列里只有一个UndoState,其newText为“adcdef";因此可见刚刚的推断是正确的,有一个合并的操作导致的问题

那么为什么会合并呢?或者说正常为什么不会合并呢? 首先看undo的流程

5978        /**
5979         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5980         * If forceMerge is true then the new edit is always merged.
5981         */
5982        private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
5983            // Fetch the last edit operation and attempt to merge in the new edit.
5984            final UndoManager um = mEditor.mUndoManager;
5985            um.beginUpdate("Edit text");
5986            EditOperation lastEdit = getLastEdit();
5987            if (lastEdit == null) {
5988                // Add this as the first edit.
5989                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5990                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5991            } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
5992                // Forced merges take priority because they could be the result of a non-user-edit
5993                // change and this case should not create a new undo operation.
5994                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5995                lastEdit.forceMergeWith(edit);
5996            } else if (!mIsUserEdit) {
5997                // An application directly modified the Editable outside of a text edit. Treat this
5998                // as a new change and don't attempt to merge.
5999                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6000                um.commitState(mEditor.mUndoOwner);
6001                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
6002            } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
6003                // Merge succeeded, nothing else to do.
6004                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
6005            } else {
6006                // Could not merge with the last edit, so commit the last edit and add this edit.
6007                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6008                um.commitState(mEditor.mUndoOwner);
6009                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
6010            }
6011            mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
6012            um.endUpdate();
6013        }

看注释,fetches the last undo operation and checks to see if a new edit should be merged into it;这里决定了是否合并

602    /**
603     * Commit the last finished undo state.  This undo state can no longer be
604     * modified with further {@link #MERGE_MODE_UNIQUE} or
605     * {@link #MERGE_MODE_ANY} merge modes.  If called while inside of an update,
606     * this will push any changes in the current update on to the undo stack
607     * and result with a fresh undo state, behaving as if {@link #endUpdate()}
608     * had been called enough to unwind the current update, then the last state
609     * committed, and {@link #beginUpdate} called to restore the update nesting.
610     * @param owner The optional owner to determine whether to perform the commit.
611     * If this is non-null, the commit will only execute if the current top undo
612     * state contains an operation with the given owner.
613     * @return Returns an integer identifier for the committed undo state, which
614     * can later be used to try to uncommit the state to perform further edits on it.
615     */
616    public int commitState(UndoOwner owner) {
617        if (mWorking != null && mWorking.hasData()) {
618            if (owner == null || mWorking.hasOperation(owner)) {
619                mWorking.setCanMerge(false);
620                int commitId = mWorking.getCommitId();
621                pushWorkingState();
622                createWorkingState();
623                mMerged = true;
624                return commitId;
625            }
626        } else {
627            UndoState state = getTopUndo(null);
628            if (state != null && (owner == null || state.hasOperation(owner))) {
629                state.setCanMerge(false);
630                return state.getCommitId();
631            }
632        }
633        return -1;
634    }

当调用到commitState时,会将CanMerge设为false,那么就不会merge了;剩下的就是调试与分析工作了;

发现正常情况下,Editor的mIsUserEdit为false

5856        // Whether the current filter pass is directly caused by an end-user text edit.
5857        private boolean mIsUserEdit;
 
 
5887        /** 5888 * Signals that a user-triggered edit is starting. 5889 */
5890        public void beginBatchEdit() {
5891            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5892            mIsUserEdit = true;
5893        }
5894
5895        public void endBatchEdit() {
5896            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5897            mIsUserEdit = false;
5898            mPreviousOperationWasInSameBatchEdit = false;
5899        }

开始edit时为true,结束edit时为false;凭直觉这里也应该成对出现吧;说明肯定哪个地方有了时序上的错乱导致的问题;

调试后发现了,果然出了正常的逻辑的key down,append的逻辑外,fail机器还有一个关键的地方会调用


editor1.png

这个消息的发送处在

public void beginBatchEdit() {
    dispatchMessage(obtainMessage(DO_BEGIN_BATCH_EDIT));
}
editor2.png
editor3.png

可以看到是输入法进行了调用,因为是不同进程进行的操作,是有可能造成时序的错乱,当执行到关键位置时,mIsUserEdit = true;导致将两次的操作合并了,因此undo时直接全部回退,case失败;

然后将输入法换成百度的,一测,果然必pass;因此确定是sogou输入法的问题

问题总结

输入法还可能造成Editor的相关fail。虽然表现在Editor上,但未必是其本身的问题;这里只是简单的定位问题,具体该如何修改,需要sogou的同学来看下其内部的实现逻辑了,这个binder call到底是什么情况会调用。

你可能感兴趣的:(CTS问题分析16)