美文网首页
CTS问题分析16

CTS问题分析16

作者: weiinter105 | 来源:发表于2019-03-13 17:50 被阅读0次

    最近好像和输入法比较有缘啊,又是一个定制的输入法造成的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 <var>count</var> undo states.  The states impacted
    226     * by this can be limited through <var>owners</var>.
    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

          本文链接:https://www.haomeiwen.com/subject/phbtmqtx.html