最近好像和输入法比较有缘啊,又是一个定制的输入法造成的CTS问题;话说,一般第三方app造成CTS问题的情况一般是危险权限之类的,输入法顶多是window遮挡了uiautomator待识别的控件;今天这个问题还真是以前没见过的,因此记录下这个问题
问题初探
测试命令: run cts -m CtsWidgetTestCases -t android.widget.cts.TextViewTest#testUndo_directAppend
测试case如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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操作开始看起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
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操作
1 2 3 4 5 6 7 |
365 void undo() { 366 if (!mAllowUndo) { 367 return; 368 } 369 UndoOwner[] owners = { mUndoOwner }; 370 mUndoManager.undo(owners, 1); // Undo 1 action. 371 } |
就是调用UndoManager的undo操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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时,其结果不正确
正常情况下应该为
但是实际上fail时undo队列里只有一个UndoState,其newText为“adcdef";因此可见刚刚的推断是正确的,有一个合并的操作导致的问题
那么为什么会合并呢?或者说正常为什么不会合并呢? 首先看undo的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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;这里决定了是否合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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机器还有一个关键的地方会调用
这个消息的发送处在
1 2 3 |
public void beginBatchEdit() { dispatchMessage(obtainMessage(DO_BEGIN_BATCH_EDIT)); } |
可以看到是输入法进行了调用,因为是不同进程进行的操作,是有可能造成时序的错乱,当执行到关键位置时,mIsUserEdit = true;导致将两次的操作合并了,因此undo时直接全部回退,case失败;
然后将输入法换成百度的,一测,果然必pass;因此确定是sogou输入法的问题
问题总结
输入法还可能造成Editor的相关fail。虽然表现在Editor上,但未必是其本身的问题;这里只是简单的定位问题,具体该如何修改,需要sogou的同学来看下其内部的实现逻辑了,这个binder call到底是什么情况会调用。