最近好像和输入法比较有缘啊,又是一个定制的输入法造成的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到底是什么情况会调用。
网友评论