1 /*
2  * Copyright (C) 2015 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 
17 package com.android.tv.tuner.cc;
18 
19 import android.os.Handler;
20 import android.os.Message;
21 import android.util.Log;
22 import android.view.View;
23 
24 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
25 import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
26 import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
27 import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
28 import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
29 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
30 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
31 
32 import java.util.ArrayList;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * Decodes and renders CEA-708.
37  */
38 public class CaptionTrackRenderer implements Handler.Callback {
39     // TODO: Remaining works
40     // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are
41     // described in the follows.
42     // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized but
43     //           it is handled as EUC-KR charset for korea broadcasting.
44     // C1 Table: All styles of windows and pens except underline, italic, pen size, and pen offset
45     //           specified in CEA-708 are ignored and this follows system wide cc preferences for
46     //           look and feel. SetPenLocation is not implemented.
47     // G2 Table: TSP, NBTSP and BLK are not supported.
48     // Text/commands: Word wrapping, fonts, row and column locking are not supported.
49 
50     private static final String TAG = "CaptionTrackRenderer";
51     private static final boolean DEBUG = false;
52 
53     private static final long DELAY_IN_MILLIS = TimeUnit.MILLISECONDS.toMillis(100);
54 
55     // According to CEA-708B, there can exist up to 8 caption windows.
56     private static final int CAPTION_WINDOWS_MAX = 8;
57     private static final int CAPTION_ALL_WINDOWS_BITMAP = 255;
58 
59     private static final int MSG_DELAY_CANCEL = 1;
60     private static final int MSG_CAPTION_CLEAR = 2;
61 
62     private static final long CAPTION_CLEAR_INTERVAL_MS = 60000;
63 
64     private final CaptionLayout mCaptionLayout;
65     private boolean mIsDelayed = false;
66     private CaptionWindowLayout mCurrentWindowLayout;
67     private final CaptionWindowLayout[] mCaptionWindowLayouts =
68             new CaptionWindowLayout[CAPTION_WINDOWS_MAX];
69     private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>();
70     private final Handler mHandler;
71 
CaptionTrackRenderer(CaptionLayout captionLayout)72     public CaptionTrackRenderer(CaptionLayout captionLayout) {
73         mCaptionLayout = captionLayout;
74         mHandler = new Handler(this);
75     }
76 
77     @Override
handleMessage(Message msg)78     public boolean handleMessage(Message msg) {
79         switch (msg.what) {
80             case MSG_DELAY_CANCEL:
81                 delayCancel();
82                 return true;
83             case MSG_CAPTION_CLEAR:
84                 clearWindows(CAPTION_ALL_WINDOWS_BITMAP);
85                 return true;
86         }
87         return false;
88     }
89 
start(AtscCaptionTrack captionTrack)90     public void start(AtscCaptionTrack captionTrack) {
91         if (captionTrack == null) {
92             stop();
93             return;
94         }
95         if (DEBUG) {
96             Log.d(TAG, "Start captionTrack " + captionTrack.language);
97         }
98         reset();
99         mCaptionLayout.setCaptionTrack(captionTrack);
100         mCaptionLayout.setVisibility(View.VISIBLE);
101     }
102 
stop()103     public void stop() {
104         if (DEBUG) {
105             Log.d(TAG, "Stop captionTrack");
106         }
107         mCaptionLayout.setVisibility(View.INVISIBLE);
108         mHandler.removeMessages(MSG_CAPTION_CLEAR);
109     }
110 
processCaptionEvent(CaptionEvent event)111     public void processCaptionEvent(CaptionEvent event) {
112         if (mIsDelayed) {
113             mPendingCaptionEvents.add(event);
114             return;
115         }
116         switch (event.type) {
117             case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER:
118                 sendBufferToCurrentWindow((String) event.obj);
119                 break;
120             case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL:
121                 sendControlToCurrentWindow((char) event.obj);
122                 break;
123             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX:
124                 setCurrentWindowLayout((int) event.obj);
125                 break;
126             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW:
127                 clearWindows((int) event.obj);
128                 break;
129             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW:
130                 displayWindows((int) event.obj);
131                 break;
132             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW:
133                 hideWindows((int) event.obj);
134                 break;
135             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW:
136                 toggleWindows((int) event.obj);
137                 break;
138             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW:
139                 deleteWindows((int) event.obj);
140                 break;
141             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY:
142                 delay((int) event.obj);
143                 break;
144             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC:
145                 delayCancel();
146                 break;
147             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST:
148                 reset();
149                 break;
150             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA:
151                 setPenAttr((CaptionPenAttr) event.obj);
152                 break;
153             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC:
154                 setPenColor((CaptionPenColor) event.obj);
155                 break;
156             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL:
157                 setPenLocation((CaptionPenLocation) event.obj);
158                 break;
159             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA:
160                 setWindowAttr((CaptionWindowAttr) event.obj);
161                 break;
162             case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX:
163                 defineWindow((CaptionWindow) event.obj);
164                 break;
165         }
166     }
167 
168     // The window related caption commands
setCurrentWindowLayout(int windowId)169     private void setCurrentWindowLayout(int windowId) {
170         if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
171             return;
172         }
173         CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
174         if (windowLayout == null) {
175             return;
176         }
177         if (DEBUG) {
178             Log.d(TAG, "setCurrentWindowLayout to " + windowId);
179         }
180         mCurrentWindowLayout = windowLayout;
181     }
182 
183     // Each bit of windowBitmap indicates a window.
184     // If a bit is set, the window id is the same as the number of the trailing zeros of the bit.
getWindowsFromBitmap(int windowBitmap)185     private ArrayList<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) {
186         ArrayList<CaptionWindowLayout> windows = new ArrayList<>();
187         for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
188             if ((windowBitmap & (1 << i)) != 0) {
189                 CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i];
190                 if (windowLayout != null) {
191                     windows.add(windowLayout);
192                 }
193             }
194         }
195         return windows;
196     }
197 
clearWindows(int windowBitmap)198     private void clearWindows(int windowBitmap) {
199         if (windowBitmap == 0) {
200             return;
201         }
202         for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
203             windowLayout.clear();
204         }
205     }
206 
displayWindows(int windowBitmap)207     private void displayWindows(int windowBitmap) {
208         if (windowBitmap == 0) {
209             return;
210         }
211         for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
212             windowLayout.show();
213         }
214     }
215 
hideWindows(int windowBitmap)216     private void hideWindows(int windowBitmap) {
217         if (windowBitmap == 0) {
218             return;
219         }
220         for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
221             windowLayout.hide();
222         }
223     }
224 
toggleWindows(int windowBitmap)225     private void toggleWindows(int windowBitmap) {
226         if (windowBitmap == 0) {
227             return;
228         }
229         for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
230             if (windowLayout.isShown()) {
231                 windowLayout.hide();
232             } else {
233                 windowLayout.show();
234             }
235         }
236     }
237 
deleteWindows(int windowBitmap)238     private void deleteWindows(int windowBitmap) {
239         if (windowBitmap == 0) {
240             return;
241         }
242         for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) {
243             windowLayout.removeFromCaptionView();
244             mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null;
245         }
246     }
247 
reset()248     public void reset() {
249         mCurrentWindowLayout = null;
250         mIsDelayed = false;
251         mPendingCaptionEvents.clear();
252         for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) {
253             if (mCaptionWindowLayouts[i] != null) {
254                 mCaptionWindowLayouts[i].removeFromCaptionView();
255             }
256             mCaptionWindowLayouts[i] = null;
257         }
258         mCaptionLayout.setVisibility(View.INVISIBLE);
259         mHandler.removeMessages(MSG_CAPTION_CLEAR);
260     }
261 
setWindowAttr(CaptionWindowAttr windowAttr)262     private void setWindowAttr(CaptionWindowAttr windowAttr) {
263         if (mCurrentWindowLayout != null) {
264             mCurrentWindowLayout.setWindowAttr(windowAttr);
265         }
266     }
267 
defineWindow(CaptionWindow window)268     private void defineWindow(CaptionWindow window) {
269         if (window == null) {
270             return;
271         }
272         int windowId = window.id;
273         if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) {
274             return;
275         }
276         CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId];
277         if (windowLayout == null) {
278             windowLayout = new CaptionWindowLayout(mCaptionLayout.getContext());
279         }
280         windowLayout.initWindow(mCaptionLayout, window);
281         mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout;
282     }
283 
284     // The job related caption commands
delay(int tenthsOfSeconds)285     private void delay(int tenthsOfSeconds) {
286         if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) {
287             return;
288         }
289         mIsDelayed = true;
290         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL),
291                 tenthsOfSeconds * DELAY_IN_MILLIS);
292     }
293 
delayCancel()294     private void delayCancel() {
295         mIsDelayed = false;
296         processPendingBuffer();
297     }
298 
processPendingBuffer()299     private void processPendingBuffer() {
300         for (CaptionEvent event : mPendingCaptionEvents) {
301             processCaptionEvent(event);
302         }
303         mPendingCaptionEvents.clear();
304     }
305 
306     // The implicit write caption commands
sendControlToCurrentWindow(char control)307     private void sendControlToCurrentWindow(char control) {
308         if (mCurrentWindowLayout != null) {
309             mCurrentWindowLayout.sendControl(control);
310         }
311     }
312 
sendBufferToCurrentWindow(String buffer)313     private void sendBufferToCurrentWindow(String buffer) {
314         if (mCurrentWindowLayout != null) {
315             mCurrentWindowLayout.sendBuffer(buffer);
316             mHandler.removeMessages(MSG_CAPTION_CLEAR);
317             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR),
318                     CAPTION_CLEAR_INTERVAL_MS);
319         }
320     }
321 
322     // The pen related caption commands
setPenAttr(CaptionPenAttr attr)323     private void setPenAttr(CaptionPenAttr attr) {
324         if (mCurrentWindowLayout != null) {
325             mCurrentWindowLayout.setPenAttr(attr);
326         }
327     }
328 
setPenColor(CaptionPenColor color)329     private void setPenColor(CaptionPenColor color) {
330         if (mCurrentWindowLayout != null) {
331             mCurrentWindowLayout.setPenColor(color);
332         }
333     }
334 
setPenLocation(CaptionPenLocation location)335     private void setPenLocation(CaptionPenLocation location) {
336         if (mCurrentWindowLayout != null) {
337             mCurrentWindowLayout.setPenLocation(location.row, location.column);
338         }
339     }
340 }
341