PARAGRAPH span must start at paragraph boundary

線上錯誤日志

2018-11-01 11:18:29.519 21987-21987/xxx.xxx.xx E/MtaSDK.CaughtExp: java.lang.RuntimeException: PARAGRAPH span must end at paragraph boundary (62 follows  )
        at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:171)
        at android.text.SpannableStringInternal.copySpans(SpannableStringInternal.java:68)
        at android.text.SpannableStringInternal.<init>(SpannableStringInternal.java:43)
        at android.text.SpannedString.<init>(SpannedString.java:30)
        at android.text.method.ReplacementTransformationMethod$SpannedReplacementCharSequence.subSequence(ReplacementTransformationMethod.java:180)
        at android.widget.TextView.getTransformedText(TextView.java:9529)
        at android.widget.TextView.onTextContextMenuItem(TextView.java:9484)
        at android.widget.Editor$TextActionModeCallback.onActionItemClicked(Editor.java:4031)
        at com.android.internal.policy.DecorView$ActionModeCallback2Wrapper.onActionItemClicked(DecorView.java:2393)
        at com.android.internal.view.FloatingActionMode$3.onMenuItemSelected(FloatingActionMode.java:88)
        at com.android.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:761)
        at com.android.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:152)
        at com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:904)
        at com.android.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:894)
        at com.android.internal.view.FloatingActionMode$4.onMenuItemClick(FloatingActionMode.java:114)
        at com.android.internal.widget.FloatingToolbar$FloatingToolbarPopup$3.onClick(FloatingToolbar.java:398)
        at android.view.View.performClick(View.java:5642)
        at android.view.View$PerformClick.run(View.java:22485)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6211)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:793)

復現場景的話是復制類似情況下的文字然后粘貼進應用內的編輯框蚊锹,然后再次進行復制,報錯。但是這是后期根據分析才找出來的復現路徑攻礼,那么在沒有復現場景的時候怎么分析呢


一層一層尋找報錯源頭胖齐,這次先從報錯的方法入手玻淑,查找SpannableStringInternal中有關PARAGRAPH的代碼,可以發(fā)現

/* package */ void setSpan(Object what, int start, int end, int flags) {
        int nstart = start;
        int nend = end;

        checkRange("setSpan", start, end);

        if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
            if (start != 0 && start != length()) {
                char c = charAt(start - 1);

                if (c != '\n')
                    throw new RuntimeException(
                            "PARAGRAPH span must start at paragraph boundary" +
                            " (" + start + " follows " + c + ")");
            }

            if (end != 0 && end != length()) {
                char c = charAt(end - 1);

                if (c != '\n')
                    throw new RuntimeException(
                            "PARAGRAPH span must end at paragraph boundary" +
                            " (" + end + " follows " + c + ")");
            }
        }
    ...

從邏輯來看是當標簽為Spannable.SPAN_PARAGRAPH時檢測頭部以及尾部是否含有\(zhòng)n呀伙,如果沒有的話會報錯
再看下他在哪調用的

  /**
     * Copies another {@link Spanned} object's spans between [start, end] into this object.
     *
     * @param src Source object to copy from.
     * @param start Start index in the source object.
     * @param end End index in the source object.
     */
    private final void copySpans(Spanned src, int start, int end) {
        Object[] spans = src.getSpans(start, end, Object.class);

        for (int i = 0; i < spans.length; i++) {
            int st = src.getSpanStart(spans[i]);
            int en = src.getSpanEnd(spans[i]);
            int fl = src.getSpanFlags(spans[i]);

            if (st < start)
                st = start;
            if (en > end)
                en = end;

            setSpan(spans[i], st - start, en - start, fl);
        }
    }

從注釋來看补履,是將src中的樣式統(tǒng)統(tǒng)復制一遍,f1就是樣式標簽剿另,其中就包括上面需要檢測的Spannable.SPAN_PARAGRAPH箫锤,再次向上查看調用

    /* package */ SpannableStringInternal(CharSequence source,
                                          int start, int end) {
        if (start == 0 && end == source.length())
            mText = source.toString();
        else
            mText = source.toString().substring(start, end);

        mSpans = EmptyArray.OBJECT;
        mSpanData = EmptyArray.INT;

        if (source instanceof Spanned) {
            if (source instanceof SpannableStringInternal) {
                copySpans((SpannableStringInternal) source, start, end);
            } else {
                copySpans((Spanned) source, start, end);
            }
        }
    }

在實例化的時候將傳入的source中的樣式復制,這里已經是這個類中能追蹤到最起始的位置雨女,下面還要繼續(xù)追蹤的話需要通過查看調用棧谚攒,找到引用的地方

ReplacementTransformationMethod.java
    private static class SpannedReplacementCharSequence
    extends ReplacementCharSequence
    implements Spanned
    {
        public SpannedReplacementCharSequence(Spanned source, char[] original,
                                              char[] replacement) {
            super(source, original, replacement);
            mSpanned = source;
        }

        public CharSequence subSequence(int start, int end) {
            return new SpannedString(this).subSequence(start, end);
        }

        public <T> T[] getSpans(int start, int end, Class<T> type) {
            return mSpanned.getSpans(start, end, type);
        }

        public int getSpanStart(Object tag) {
            return mSpanned.getSpanStart(tag);
        }

        public int getSpanEnd(Object tag) {
            return mSpanned.getSpanEnd(tag);
        }

        public int getSpanFlags(Object tag) {
            return mSpanned.getSpanFlags(tag);
        }

        public int nextSpanTransition(int start, int end, Class type) {
            return mSpanned.nextSpanTransition(start, end, type);
        }

        private Spanned mSpanned;
    }

subSequence,很熟悉的方法氛堕,將 CharSequence在指定部分切割

    public boolean onTextContextMenuItem(int id) {
        int min = 0;
        int max = mText.length();

        if (isFocused()) {
            final int selStart = getSelectionStart();
            final int selEnd = getSelectionEnd();

            min = Math.max(0, Math.min(selStart, selEnd));
            max = Math.max(0, Math.max(selStart, selEnd));
        }

        switch (id) {
            case ID_SELECT_ALL:
                selectAllText();
                return true;

            case ID_UNDO:
                if (mEditor != null) {
                    mEditor.undo();
                }
                return true;  // Returns true even if nothing was undone.

            case ID_REDO:
                if (mEditor != null) {
                    mEditor.redo();
                }
                return true;  // Returns true even if nothing was undone.

            case ID_PASTE:
                paste(min, max, true /* withFormatting */);
                return true;

            case ID_PASTE_AS_PLAIN_TEXT:
                paste(min, max, false /* withFormatting */);
                return true;

            case ID_CUT:
                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
                deleteText_internal(min, max);
                return true;

            case ID_COPY:
                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
                stopTextActionMode();
                return true;

            case ID_REPLACE:
                if (mEditor != null) {
                    mEditor.replace();
                }
                return true;

            case ID_SHARE:
                shareSelectedText();
                return true;
        }
        return false;
    }

    CharSequence getTransformedText(int start, int end) {
        return removeSuggestionSpans(mTransformed.subSequence(start, end));
    }

到這里流程就很明了了馏臭,在復制時將TextView中選中的部分進行帶樣式的復制,那么肯定是TextView中的文本不規(guī)范導致復制時報錯岔擂∥晃梗考慮到應用內使用時沒有加入Spannable.SPAN_PARAGRAPH這樣的標簽,只有可能是通過復制外部文本粘貼進EditText中導致問題乱灵,所以解決方法也很簡單塑崖,提前攔截復制剪切的處理邏輯,改成純文本復制痛倚。

@Override
    public boolean onTextContextMenuItem(int id) {
        try {
            return super.onTextContextMenuItem(id);
        } catch (RuntimeException e) {
            if (getText() == null){
                return false;
            }
            int min = 0;
            int max = getText().length();
            if (id == android.R.id.cut) {
                if (isFocused()) {
                    final int selStart = getSelectionStart();
                    final int selEnd = getSelectionEnd();
                    min = Math.max(0, Math.min(selStart, selEnd));
                    max = Math.max(0, Math.max(selStart, selEnd));
                }
                ClipData cutData = ClipData.newPlainText(null, getText().toString().subSequence(min, max));
                if (setPrimaryClip(cutData)) {
                    getText().delete(min, max);
                }
                return true;
            } else if (id == android.R.id.copy) {
                final int selStart = getSelectionStart();
                final int selEnd = getSelectionEnd();
                min = Math.max(0, Math.min(selStart, selEnd));
                max = Math.max(0, Math.max(selStart, selEnd));
                ClipData copyData = ClipData.newPlainText(null, getText().toString().subSequence(min, max));
                if (setPrimaryClip(copyData)) {
                    //通過setText隱藏FloatingMenu
                    setText(getText());
                }
                return true;
            }
        }
        return false;
    }

    private boolean setPrimaryClip(ClipData clip) {
        ClipboardManager clipboard =
                (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
        if (clipboard == null){
            return false;
        }
        try {
            clipboard.setPrimaryClip(clip);
        } catch (Throwable t) {
            return false;
        }
        return true;
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末规婆,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子蝉稳,更是在濱河造成了極大的恐慌抒蚜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耘戚,死亡現場離奇詭異嗡髓,居然都是意外死亡,警方通過查閱死者的電腦和手機收津,發(fā)現死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門饿这,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浊伙,“玉大人,你說我怎么就攤上這事长捧∠桑” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵串结,是天一觀的道長哑子。 經常有香客問我,道長肌割,這世上最難降的妖魔是什么卧蜓? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮声功,結果婚禮上烦却,老公的妹妹穿的比我還像新娘宠叼。我一直安慰自己先巴,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布冒冬。 她就那樣靜靜地躺著伸蚯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪简烤。 梳的紋絲不亂的頭發(fā)上剂邮,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音横侦,去河邊找鬼挥萌。 笑死,一個胖子當著我的面吹牛枉侧,可吹牛的內容都是我干的引瀑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼榨馁,長吁一口氣:“原來是場噩夢啊……” “哼憨栽!你這毒婦竟也來了?” 一聲冷哼從身側響起翼虫,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤屑柔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后珍剑,有當地人在樹林里發(fā)現了一具尸體掸宛,經...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年招拙,在試婚紗的時候發(fā)現自己被綠了唧瘾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翔曲。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖劈愚,靈堂內的尸體忽然破棺而出瞳遍,到底是詐尸還是另有隱情,我是刑警寧澤菌羽,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布掠械,位于F島的核電站,受9級特大地震影響注祖,放射性物質發(fā)生泄漏猾蒂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一是晨、第九天 我趴在偏房一處隱蔽的房頂上張望肚菠。 院中可真熱鬧,春花似錦罩缴、人聲如沸蚊逢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烙荷。三九已至,卻和暖如春檬寂,著一層夾襖步出監(jiān)牢的瞬間终抽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工桶至, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昼伴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓镣屹,卻偏偏與公主長得像圃郊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子野瘦,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354