1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.media.subtitle;
18
19import android.text.Spannable;
20import android.text.SpannableStringBuilder;
21import android.text.TextPaint;
22import android.text.style.CharacterStyle;
23import android.text.style.StyleSpan;
24import android.text.style.UnderlineSpan;
25import android.text.style.UpdateAppearance;
26import android.util.Log;
27import android.view.accessibility.CaptioningManager.CaptionStyle;
28
29import java.util.ArrayList;
30import java.util.Arrays;
31
32/**
33 * CCParser processes CEA-608 closed caption data.
34 *
35 * It calls back into OnDisplayChangedListener upon
36 * display change with styled text for rendering.
37 *
38 */
39class Cea608CCParser {
40    public static final int MAX_ROWS = 15;
41    public static final int MAX_COLS = 32;
42
43    private static final String TAG = "Cea608CCParser";
44    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
45
46    private static final int INVALID = -1;
47
48    // EIA-CEA-608: Table 70 - Control Codes
49    private static final int RCL = 0x20;
50    private static final int BS  = 0x21;
51    private static final int AOF = 0x22;
52    private static final int AON = 0x23;
53    private static final int DER = 0x24;
54    private static final int RU2 = 0x25;
55    private static final int RU3 = 0x26;
56    private static final int RU4 = 0x27;
57    private static final int FON = 0x28;
58    private static final int RDC = 0x29;
59    private static final int TR  = 0x2a;
60    private static final int RTD = 0x2b;
61    private static final int EDM = 0x2c;
62    private static final int CR  = 0x2d;
63    private static final int ENM = 0x2e;
64    private static final int EOC = 0x2f;
65
66    // Transparent Space
67    private static final char TS = '\u00A0';
68
69    // Captioning Modes
70    private static final int MODE_UNKNOWN = 0;
71    private static final int MODE_PAINT_ON = 1;
72    private static final int MODE_ROLL_UP = 2;
73    private static final int MODE_POP_ON = 3;
74    private static final int MODE_TEXT = 4;
75
76    private final DisplayListener mListener;
77
78    private int mMode = MODE_PAINT_ON;
79    private int mRollUpSize = 4;
80    private int mPrevCtrlCode = INVALID;
81
82    private CCMemory mDisplay = new CCMemory();
83    private CCMemory mNonDisplay = new CCMemory();
84    private CCMemory mTextMem = new CCMemory();
85
86    Cea608CCParser(DisplayListener listener) {
87        mListener = listener;
88    }
89
90    public void parse(byte[] data) {
91        CCData[] ccData = CCData.fromByteArray(data);
92
93        for (int i = 0; i < ccData.length; i++) {
94            if (DEBUG) {
95                Log.d(TAG, ccData[i].toString());
96            }
97
98            if (handleCtrlCode(ccData[i])
99                    || handleTabOffsets(ccData[i])
100                    || handlePACCode(ccData[i])
101                    || handleMidRowCode(ccData[i])) {
102                continue;
103            }
104
105            handleDisplayableChars(ccData[i]);
106        }
107    }
108
109    interface DisplayListener {
110        void onDisplayChanged(SpannableStringBuilder[] styledTexts);
111        CaptionStyle getCaptionStyle();
112    }
113
114    private CCMemory getMemory() {
115        // get the CC memory to operate on for current mode
116        switch (mMode) {
117            case MODE_POP_ON:
118                return mNonDisplay;
119            case MODE_TEXT:
120                // TODO(chz): support only caption mode for now,
121                // in text mode, dump everything to text mem.
122                return mTextMem;
123            case MODE_PAINT_ON:
124            case MODE_ROLL_UP:
125                return mDisplay;
126            default:
127                Log.w(TAG, "unrecoginized mode: " + mMode);
128        }
129        return mDisplay;
130    }
131
132    private boolean handleDisplayableChars(CCData ccData) {
133        if (!ccData.isDisplayableChar()) {
134            return false;
135        }
136
137        // Extended char includes 1 automatic backspace
138        if (ccData.isExtendedChar()) {
139            getMemory().bs();
140        }
141
142        getMemory().writeText(ccData.getDisplayText());
143
144        if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
145            updateDisplay();
146        }
147
148        return true;
149    }
150
151    private boolean handleMidRowCode(CCData ccData) {
152        StyleCode m = ccData.getMidRow();
153        if (m != null) {
154            getMemory().writeMidRowCode(m);
155            return true;
156        }
157        return false;
158    }
159
160    private boolean handlePACCode(CCData ccData) {
161        PAC pac = ccData.getPAC();
162
163        if (pac != null) {
164            if (mMode == MODE_ROLL_UP) {
165                getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
166            }
167            getMemory().writePAC(pac);
168            return true;
169        }
170
171        return false;
172    }
173
174    private boolean handleTabOffsets(CCData ccData) {
175        int tabs = ccData.getTabOffset();
176
177        if (tabs > 0) {
178            getMemory().tab(tabs);
179            return true;
180        }
181
182        return false;
183    }
184
185    private boolean handleCtrlCode(CCData ccData) {
186        int ctrlCode = ccData.getCtrlCode();
187
188        if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
189            // discard double ctrl codes (but if there's a 3rd one, we still take that)
190            mPrevCtrlCode = INVALID;
191            return true;
192        }
193
194        switch(ctrlCode) {
195            case RCL:
196                // select pop-on style
197                mMode = MODE_POP_ON;
198                break;
199            case BS:
200                getMemory().bs();
201                break;
202            case DER:
203                getMemory().der();
204                break;
205            case RU2:
206            case RU3:
207            case RU4:
208                mRollUpSize = (ctrlCode - 0x23);
209                // erase memory if currently in other style
210                if (mMode != MODE_ROLL_UP) {
211                    mDisplay.erase();
212                    mNonDisplay.erase();
213                }
214                // select roll-up style
215                mMode = MODE_ROLL_UP;
216                break;
217            case FON:
218                Log.i(TAG, "Flash On");
219                break;
220            case RDC:
221                // select paint-on style
222                mMode = MODE_PAINT_ON;
223                break;
224            case TR:
225                mMode = MODE_TEXT;
226                mTextMem.erase();
227                break;
228            case RTD:
229                mMode = MODE_TEXT;
230                break;
231            case EDM:
232                // erase display memory
233                mDisplay.erase();
234                updateDisplay();
235                break;
236            case CR:
237                if (mMode == MODE_ROLL_UP) {
238                    getMemory().rollUp(mRollUpSize);
239                } else {
240                    getMemory().cr();
241                }
242                if (mMode == MODE_ROLL_UP) {
243                    updateDisplay();
244                }
245                break;
246            case ENM:
247                // erase non-display memory
248                mNonDisplay.erase();
249                break;
250            case EOC:
251                // swap display/non-display memory
252                swapMemory();
253                // switch to pop-on style
254                mMode = MODE_POP_ON;
255                updateDisplay();
256                break;
257            case INVALID:
258            default:
259                mPrevCtrlCode = INVALID;
260                return false;
261        }
262
263        mPrevCtrlCode = ctrlCode;
264
265        // handled
266        return true;
267    }
268
269    private void updateDisplay() {
270        if (mListener != null) {
271            CaptionStyle captionStyle = mListener.getCaptionStyle();
272            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
273        }
274    }
275
276    private void swapMemory() {
277        CCMemory temp = mDisplay;
278        mDisplay = mNonDisplay;
279        mNonDisplay = temp;
280    }
281
282    private static class StyleCode {
283        static final int COLOR_WHITE = 0;
284        static final int COLOR_GREEN = 1;
285        static final int COLOR_BLUE = 2;
286        static final int COLOR_CYAN = 3;
287        static final int COLOR_RED = 4;
288        static final int COLOR_YELLOW = 5;
289        static final int COLOR_MAGENTA = 6;
290        static final int COLOR_INVALID = 7;
291
292        static final int STYLE_ITALICS   = 0x00000001;
293        static final int STYLE_UNDERLINE = 0x00000002;
294
295        static final String[] sColorMap = {
296            "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
297        };
298
299        final int mStyle;
300        final int mColor;
301
302        static StyleCode fromByte(byte data2) {
303            int style = 0;
304            int color = (data2 >> 1) & 0x7;
305
306            if ((data2 & 0x1) != 0) {
307                style |= STYLE_UNDERLINE;
308            }
309
310            if (color == COLOR_INVALID) {
311                // WHITE ITALICS
312                color = COLOR_WHITE;
313                style |= STYLE_ITALICS;
314            }
315
316            return new StyleCode(style, color);
317        }
318
319        StyleCode(int style, int color) {
320            mStyle = style;
321            mColor = color;
322        }
323
324        boolean isItalics() {
325            return (mStyle & STYLE_ITALICS) != 0;
326        }
327
328        boolean isUnderline() {
329            return (mStyle & STYLE_UNDERLINE) != 0;
330        }
331
332        int getColor() {
333            return mColor;
334        }
335
336        @Override
337        public String toString() {
338            StringBuilder str = new StringBuilder();
339            str.append("{");
340            str.append(sColorMap[mColor]);
341            if ((mStyle & STYLE_ITALICS) != 0) {
342                str.append(", ITALICS");
343            }
344            if ((mStyle & STYLE_UNDERLINE) != 0) {
345                str.append(", UNDERLINE");
346            }
347            str.append("}");
348
349            return str.toString();
350        }
351    }
352
353    private static class PAC extends StyleCode {
354        final int mRow;
355        final int mCol;
356
357        static PAC fromBytes(byte data1, byte data2) {
358            int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
359            int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
360            int style = 0;
361            if ((data2 & 1) != 0) {
362                style |= STYLE_UNDERLINE;
363            }
364            if ((data2 & 0x10) != 0) {
365                // indent code
366                int indent = (data2 >> 1) & 0x7;
367                return new PAC(row, indent * 4, style, COLOR_WHITE);
368            } else {
369                // style code
370                int color = (data2 >> 1) & 0x7;
371
372                if (color == COLOR_INVALID) {
373                    // WHITE ITALICS
374                    color = COLOR_WHITE;
375                    style |= STYLE_ITALICS;
376                }
377                return new PAC(row, -1, style, color);
378            }
379        }
380
381        PAC(int row, int col, int style, int color) {
382            super(style, color);
383            mRow = row;
384            mCol = col;
385        }
386
387        boolean isIndentPAC() {
388            return (mCol >= 0);
389        }
390
391        int getRow() {
392            return mRow;
393        }
394
395        int getCol() {
396            return mCol;
397        }
398
399        @Override
400        public String toString() {
401            return String.format("{%d, %d}, %s",
402                    mRow, mCol, super.toString());
403        }
404    }
405
406    /**
407     * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
408     */
409    public static class MutableBackgroundColorSpan extends CharacterStyle
410            implements UpdateAppearance {
411        private int mColor;
412
413        MutableBackgroundColorSpan(int color) {
414            mColor = color;
415        }
416
417        public void setBackgroundColor(int color) {
418            mColor = color;
419        }
420
421        public int getBackgroundColor() {
422            return mColor;
423        }
424
425        @Override
426        public void updateDrawState(TextPaint ds) {
427            ds.bgColor = mColor;
428        }
429    }
430
431    /* CCLineBuilder keeps track of displayable chars, as well as
432     * MidRow styles and PACs, for a single line of CC memory.
433     *
434     * It generates styled text via getStyledText() method.
435     */
436    private static class CCLineBuilder {
437        private final StringBuilder mDisplayChars;
438        private final StyleCode[] mMidRowStyles;
439        private final StyleCode[] mPACStyles;
440
441        CCLineBuilder(String str) {
442            mDisplayChars = new StringBuilder(str);
443            mMidRowStyles = new StyleCode[mDisplayChars.length()];
444            mPACStyles = new StyleCode[mDisplayChars.length()];
445        }
446
447        void setCharAt(int index, char ch) {
448            mDisplayChars.setCharAt(index, ch);
449            mMidRowStyles[index] = null;
450        }
451
452        void setMidRowAt(int index, StyleCode m) {
453            mDisplayChars.setCharAt(index, ' ');
454            mMidRowStyles[index] = m;
455        }
456
457        void setPACAt(int index, PAC pac) {
458            mPACStyles[index] = pac;
459        }
460
461        char charAt(int index) {
462            return mDisplayChars.charAt(index);
463        }
464
465        int length() {
466            return mDisplayChars.length();
467        }
468
469        void applyStyleSpan(
470                SpannableStringBuilder styledText,
471                StyleCode s, int start, int end) {
472            if (s.isItalics()) {
473                styledText.setSpan(
474                        new StyleSpan(android.graphics.Typeface.ITALIC),
475                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
476            }
477            if (s.isUnderline()) {
478                styledText.setSpan(
479                        new UnderlineSpan(),
480                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
481            }
482        }
483
484        SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
485            SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
486            int start = -1, next = 0;
487            int styleStart = -1;
488            StyleCode curStyle = null;
489            while (next < mDisplayChars.length()) {
490                StyleCode newStyle = null;
491                if (mMidRowStyles[next] != null) {
492                    // apply mid-row style change
493                    newStyle = mMidRowStyles[next];
494                } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) {
495                    // apply PAC style change, only if:
496                    // 1. no style set, or
497                    // 2. style set, but prev char is none-displayable
498                    newStyle = mPACStyles[next];
499                }
500                if (newStyle != null) {
501                    curStyle = newStyle;
502                    if (styleStart >= 0 && start >= 0) {
503                        applyStyleSpan(styledText, newStyle, styleStart, next);
504                    }
505                    styleStart = next;
506                }
507
508                if (mDisplayChars.charAt(next) != TS) {
509                    if (start < 0) {
510                        start = next;
511                    }
512                } else if (start >= 0) {
513                    int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
514                    int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
515                    styledText.setSpan(
516                            new MutableBackgroundColorSpan(captionStyle.backgroundColor),
517                            expandedStart, expandedEnd,
518                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
519                    if (styleStart >= 0) {
520                        applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
521                    }
522                    start = -1;
523                }
524                next++;
525            }
526
527            return styledText;
528        }
529    }
530
531    /*
532     * CCMemory models a console-style display.
533     */
534    private static class CCMemory {
535        private final String mBlankLine;
536        private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
537        private int mRow;
538        private int mCol;
539
540        CCMemory() {
541            char[] blank = new char[MAX_COLS + 2];
542            Arrays.fill(blank, TS);
543            mBlankLine = new String(blank);
544        }
545
546        void erase() {
547            // erase all lines
548            for (int i = 0; i < mLines.length; i++) {
549                mLines[i] = null;
550            }
551            mRow = MAX_ROWS;
552            mCol = 1;
553        }
554
555        void der() {
556            if (mLines[mRow] != null) {
557                for (int i = 0; i < mCol; i++) {
558                    if (mLines[mRow].charAt(i) != TS) {
559                        for (int j = mCol; j < mLines[mRow].length(); j++) {
560                            mLines[j].setCharAt(j, TS);
561                        }
562                        return;
563                    }
564                }
565                mLines[mRow] = null;
566            }
567        }
568
569        void tab(int tabs) {
570            moveCursorByCol(tabs);
571        }
572
573        void bs() {
574            moveCursorByCol(-1);
575            if (mLines[mRow] != null) {
576                mLines[mRow].setCharAt(mCol, TS);
577                if (mCol == MAX_COLS - 1) {
578                    // Spec recommendation:
579                    // if cursor was at col 32, move cursor
580                    // back to col 31 and erase both col 31&32
581                    mLines[mRow].setCharAt(MAX_COLS, TS);
582                }
583            }
584        }
585
586        void cr() {
587            moveCursorTo(mRow + 1, 1);
588        }
589
590        void rollUp(int windowSize) {
591            int i;
592            for (i = 0; i <= mRow - windowSize; i++) {
593                mLines[i] = null;
594            }
595            int startRow = mRow - windowSize + 1;
596            if (startRow < 1) {
597                startRow = 1;
598            }
599            for (i = startRow; i < mRow; i++) {
600                mLines[i] = mLines[i + 1];
601            }
602            for (i = mRow; i < mLines.length; i++) {
603                // clear base row
604                mLines[i] = null;
605            }
606            // default to col 1, in case PAC is not sent
607            mCol = 1;
608        }
609
610        void writeText(String text) {
611            for (int i = 0; i < text.length(); i++) {
612                getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
613                moveCursorByCol(1);
614            }
615        }
616
617        void writeMidRowCode(StyleCode m) {
618            getLineBuffer(mRow).setMidRowAt(mCol, m);
619            moveCursorByCol(1);
620        }
621
622        void writePAC(PAC pac) {
623            if (pac.isIndentPAC()) {
624                moveCursorTo(pac.getRow(), pac.getCol());
625            } else {
626                moveCursorTo(pac.getRow(), 1);
627            }
628            getLineBuffer(mRow).setPACAt(mCol, pac);
629        }
630
631        SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
632            ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
633            for (int i = 1; i <= MAX_ROWS; i++) {
634                rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null);
635            }
636            return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
637        }
638
639        private static int clamp(int x, int min, int max) {
640            return x < min ? min : (x > max ? max : x);
641        }
642
643        private void moveCursorTo(int row, int col) {
644            mRow = clamp(row, 1, MAX_ROWS);
645            mCol = clamp(col, 1, MAX_COLS);
646        }
647
648        private void moveCursorToRow(int row) {
649            mRow = clamp(row, 1, MAX_ROWS);
650        }
651
652        private void moveCursorByCol(int col) {
653            mCol = clamp(mCol + col, 1, MAX_COLS);
654        }
655
656        private void moveBaselineTo(int baseRow, int windowSize) {
657            if (mRow == baseRow) {
658                return;
659            }
660            int actualWindowSize = windowSize;
661            if (baseRow < actualWindowSize) {
662                actualWindowSize = baseRow;
663            }
664            if (mRow < actualWindowSize) {
665                actualWindowSize = mRow;
666            }
667
668            int i;
669            if (baseRow < mRow) {
670                // copy from bottom to top row
671                for (i = actualWindowSize - 1; i >= 0; i--) {
672                    mLines[baseRow - i] = mLines[mRow - i];
673                }
674            } else {
675                // copy from top to bottom row
676                for (i = 0; i < actualWindowSize; i++) {
677                    mLines[baseRow - i] = mLines[mRow - i];
678                }
679            }
680            // clear rest of the rows
681            for (i = 0; i <= baseRow - windowSize; i++) {
682                mLines[i] = null;
683            }
684            for (i = baseRow + 1; i < mLines.length; i++) {
685                mLines[i] = null;
686            }
687        }
688
689        private CCLineBuilder getLineBuffer(int row) {
690            if (mLines[row] == null) {
691                mLines[row] = new CCLineBuilder(mBlankLine);
692            }
693            return mLines[row];
694        }
695    }
696
697    /*
698     * CCData parses the raw CC byte pair into displayable chars,
699     * misc control codes, Mid-Row or Preamble Address Codes.
700     */
701    private static class CCData {
702        private final byte mType;
703        private final byte mData1;
704        private final byte mData2;
705
706        private static final String[] sCtrlCodeMap = {
707            "RCL", "BS" , "AOF", "AON",
708            "DER", "RU2", "RU3", "RU4",
709            "FON", "RDC", "TR" , "RTD",
710            "EDM", "CR" , "ENM", "EOC",
711        };
712
713        private static final String[] sSpecialCharMap = {
714            "\u00AE",
715            "\u00B0",
716            "\u00BD",
717            "\u00BF",
718            "\u2122",
719            "\u00A2",
720            "\u00A3",
721            "\u266A", // Eighth note
722            "\u00E0",
723            "\u00A0", // Transparent space
724            "\u00E8",
725            "\u00E2",
726            "\u00EA",
727            "\u00EE",
728            "\u00F4",
729            "\u00FB",
730        };
731
732        private static final String[] sSpanishCharMap = {
733            // Spanish and misc chars
734            "\u00C1", // A
735            "\u00C9", // E
736            "\u00D3", // I
737            "\u00DA", // O
738            "\u00DC", // U
739            "\u00FC", // u
740            "\u2018", // opening single quote
741            "\u00A1", // inverted exclamation mark
742            "*",
743            "'",
744            "\u2014", // em dash
745            "\u00A9", // Copyright
746            "\u2120", // Servicemark
747            "\u2022", // round bullet
748            "\u201C", // opening double quote
749            "\u201D", // closing double quote
750            // French
751            "\u00C0",
752            "\u00C2",
753            "\u00C7",
754            "\u00C8",
755            "\u00CA",
756            "\u00CB",
757            "\u00EB",
758            "\u00CE",
759            "\u00CF",
760            "\u00EF",
761            "\u00D4",
762            "\u00D9",
763            "\u00F9",
764            "\u00DB",
765            "\u00AB",
766            "\u00BB"
767        };
768
769        private static final String[] sProtugueseCharMap = {
770            // Portuguese
771            "\u00C3",
772            "\u00E3",
773            "\u00CD",
774            "\u00CC",
775            "\u00EC",
776            "\u00D2",
777            "\u00F2",
778            "\u00D5",
779            "\u00F5",
780            "{",
781            "}",
782            "\\",
783            "^",
784            "_",
785            "|",
786            "~",
787            // German and misc chars
788            "\u00C4",
789            "\u00E4",
790            "\u00D6",
791            "\u00F6",
792            "\u00DF",
793            "\u00A5",
794            "\u00A4",
795            "\u2502", // vertical bar
796            "\u00C5",
797            "\u00E5",
798            "\u00D8",
799            "\u00F8",
800            "\u250C", // top-left corner
801            "\u2510", // top-right corner
802            "\u2514", // lower-left corner
803            "\u2518", // lower-right corner
804        };
805
806        static CCData[] fromByteArray(byte[] data) {
807            CCData[] ccData = new CCData[data.length / 3];
808
809            for (int i = 0; i < ccData.length; i++) {
810                ccData[i] = new CCData(
811                        data[i * 3],
812                        data[i * 3 + 1],
813                        data[i * 3 + 2]);
814            }
815
816            return ccData;
817        }
818
819        CCData(byte type, byte data1, byte data2) {
820            mType = type;
821            mData1 = data1;
822            mData2 = data2;
823        }
824
825        int getCtrlCode() {
826            if ((mData1 == 0x14 || mData1 == 0x1c)
827                    && mData2 >= 0x20 && mData2 <= 0x2f) {
828                return mData2;
829            }
830            return INVALID;
831        }
832
833        StyleCode getMidRow() {
834            // only support standard Mid-row codes, ignore
835            // optional background/foreground mid-row codes
836            if ((mData1 == 0x11 || mData1 == 0x19)
837                    && mData2 >= 0x20 && mData2 <= 0x2f) {
838                return StyleCode.fromByte(mData2);
839            }
840            return null;
841        }
842
843        PAC getPAC() {
844            if ((mData1 & 0x70) == 0x10
845                    && (mData2 & 0x40) == 0x40
846                    && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
847                return PAC.fromBytes(mData1, mData2);
848            }
849            return null;
850        }
851
852        int getTabOffset() {
853            if ((mData1 == 0x17 || mData1 == 0x1f)
854                    && mData2 >= 0x21 && mData2 <= 0x23) {
855                return mData2 & 0x3;
856            }
857            return 0;
858        }
859
860        boolean isDisplayableChar() {
861            return isBasicChar() || isSpecialChar() || isExtendedChar();
862        }
863
864        String getDisplayText() {
865            String str = getBasicChars();
866
867            if (str == null) {
868                str =  getSpecialChar();
869
870                if (str == null) {
871                    str = getExtendedChar();
872                }
873            }
874
875            return str;
876        }
877
878        private String ctrlCodeToString(int ctrlCode) {
879            return sCtrlCodeMap[ctrlCode - 0x20];
880        }
881
882        private boolean isBasicChar() {
883            return mData1 >= 0x20 && mData1 <= 0x7f;
884        }
885
886        private boolean isSpecialChar() {
887            return ((mData1 == 0x11 || mData1 == 0x19)
888                    && mData2 >= 0x30 && mData2 <= 0x3f);
889        }
890
891        private boolean isExtendedChar() {
892            return ((mData1 == 0x12 || mData1 == 0x1A
893                    || mData1 == 0x13 || mData1 == 0x1B)
894                    && mData2 >= 0x20 && mData2 <= 0x3f);
895        }
896
897        private char getBasicChar(byte data) {
898            char c;
899            // replace the non-ASCII ones
900            switch (data) {
901                case 0x2A: c = '\u00E1'; break;
902                case 0x5C: c = '\u00E9'; break;
903                case 0x5E: c = '\u00ED'; break;
904                case 0x5F: c = '\u00F3'; break;
905                case 0x60: c = '\u00FA'; break;
906                case 0x7B: c = '\u00E7'; break;
907                case 0x7C: c = '\u00F7'; break;
908                case 0x7D: c = '\u00D1'; break;
909                case 0x7E: c = '\u00F1'; break;
910                case 0x7F: c = '\u2588'; break; // Full block
911                default: c = (char) data; break;
912            }
913            return c;
914        }
915
916        private String getBasicChars() {
917            if (mData1 >= 0x20 && mData1 <= 0x7f) {
918                StringBuilder builder = new StringBuilder(2);
919                builder.append(getBasicChar(mData1));
920                if (mData2 >= 0x20 && mData2 <= 0x7f) {
921                    builder.append(getBasicChar(mData2));
922                }
923                return builder.toString();
924            }
925
926            return null;
927        }
928
929        private String getSpecialChar() {
930            if ((mData1 == 0x11 || mData1 == 0x19)
931                    && mData2 >= 0x30 && mData2 <= 0x3f) {
932                return sSpecialCharMap[mData2 - 0x30];
933            }
934
935            return null;
936        }
937
938        private String getExtendedChar() {
939            if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) {
940                // 1 Spanish/French char
941                return sSpanishCharMap[mData2 - 0x20];
942            } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) {
943                // 1 Portuguese/German/Danish char
944                return sProtugueseCharMap[mData2 - 0x20];
945            }
946
947            return null;
948        }
949
950        @Override
951        public String toString() {
952            String str;
953
954            if (mData1 < 0x10 && mData2 < 0x10) {
955                // Null Pad, ignore
956                return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
957            }
958
959            int ctrlCode = getCtrlCode();
960            if (ctrlCode != INVALID) {
961                return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
962            }
963
964            int tabOffset = getTabOffset();
965            if (tabOffset > 0) {
966                return String.format("[%d]Tab%d", mType, tabOffset);
967            }
968
969            PAC pac = getPAC();
970            if (pac != null) {
971                return String.format("[%d]PAC: %s", mType, pac.toString());
972            }
973
974            StyleCode m = getMidRow();
975            if (m != null) {
976                return String.format("[%d]Mid-row: %s", mType, m.toString());
977            }
978
979            if (isDisplayableChar()) {
980                return String.format("[%d]Displayable: %s (%02x %02x)",
981                        mType, getDisplayText(), mData1, mData2);
982            }
983
984            return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
985        }
986    }
987}
988