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.usbtuner.cc;
18 
19 import android.content.Context;
20 import android.graphics.Paint;
21 import android.graphics.Typeface;
22 import android.text.Layout.Alignment;
23 import android.text.SpannableStringBuilder;
24 import android.text.Spanned;
25 import android.text.TextUtils;
26 import android.text.style.CharacterStyle;
27 import android.text.style.RelativeSizeSpan;
28 import android.text.style.StyleSpan;
29 import android.text.style.SubscriptSpan;
30 import android.text.style.SuperscriptSpan;
31 import android.text.style.UnderlineSpan;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.Gravity;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.accessibility.CaptioningManager;
38 import android.view.accessibility.CaptioningManager.CaptionStyle;
39 import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
40 import android.widget.RelativeLayout;
41 
42 import com.google.android.exoplayer.text.CaptionStyleCompat;
43 import com.google.android.exoplayer.text.SubtitleView;
44 import com.android.usbtuner.data.Cea708Data.CaptionPenAttr;
45 import com.android.usbtuner.data.Cea708Data.CaptionPenColor;
46 import com.android.usbtuner.data.Cea708Data.CaptionWindow;
47 import com.android.usbtuner.data.Cea708Data.CaptionWindowAttr;
48 import com.android.usbtuner.layout.ScaledLayout;
49 
50 import java.nio.charset.Charset;
51 import java.nio.charset.StandardCharsets;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.List;
55 
56 /**
57  * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that
58  * takes care of displaying the actual cc text.
59  */
60 public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener {
61     private static final String TAG = "CaptionWindowLayout";
62     private static final boolean DEBUG = false;
63 
64     private static final float PROPORTION_PEN_SIZE_SMALL = .75f;
65     private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f;
66 
67     // The following values indicates the maximum cell number of a window.
68     private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99;
69     private static final int ANCHOR_VERTICAL_MAX = 74;
70     private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159;
71     private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209;
72 
73     // The following values indicates a gravity of a window.
74     private static final int ANCHOR_MODE_DIVIDER = 3;
75     private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0;
76     private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1;
77     private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2;
78     private static final int ANCHOR_VERTICAL_MODE_TOP = 0;
79     private static final int ANCHOR_VERTICAL_MODE_CENTER = 1;
80     private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2;
81 
82     private static final int US_MAX_COLUMN_COUNT_16_9 = 42;
83     private static final int US_MAX_COLUMN_COUNT_4_3 = 32;
84     private static final int KR_MAX_COLUMN_COUNT_16_9 = 52;
85     private static final int KR_MAX_COLUMN_COUNT_4_3 = 40;
86 
87     private static final String KOR_ALPHABET =
88             new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
89     private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f;
90 
91     private CaptionLayout mCaptionLayout;
92     private CaptionStyleCompat mCaptionStyleCompat;
93 
94     // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}.
95     private final SubtitleView mSubtitleView;
96     private int mRowLimit = 0;
97     private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
98     private final List<CharacterStyle> mCharacterStyles = new ArrayList<>();
99     private int mCaptionWindowId;
100     private int mRow = -1;
101     private float mFontScale;
102     private float mTextSize;
103     private String mWidestChar;
104     private int mLastCaptionLayoutWidth;
105     private int mLastCaptionLayoutHeight;
106 
107     private class SystemWideCaptioningChangeListener extends CaptioningChangeListener {
108         @Override
onUserStyleChanged(CaptionStyle userStyle)109         public void onUserStyleChanged(CaptionStyle userStyle) {
110             mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle);
111             mSubtitleView.setStyle(mCaptionStyleCompat);
112             updateWidestChar();
113         }
114 
115         @Override
onFontScaleChanged(float fontScale)116         public void onFontScaleChanged(float fontScale) {
117             mFontScale = fontScale;
118             updateTextSize();
119         }
120     }
121 
CaptionWindowLayout(Context context)122     public CaptionWindowLayout(Context context) {
123         this(context, null);
124     }
125 
CaptionWindowLayout(Context context, AttributeSet attrs)126     public CaptionWindowLayout(Context context, AttributeSet attrs) {
127         this(context, attrs, 0);
128     }
129 
CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr)130     public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
131         super(context, attrs, defStyleAttr);
132 
133         // Add a subtitle view to the layout.
134         mSubtitleView = new SubtitleView(context);
135         LayoutParams params = new RelativeLayout.LayoutParams(
136                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
137         addView(mSubtitleView, params);
138 
139         // Set the system wide cc preferences to the subtitle view.
140         CaptioningManager captioningManager =
141                 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
142         mFontScale = captioningManager.getFontScale();
143         mCaptionStyleCompat =
144                 CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
145         mSubtitleView.setStyle(mCaptionStyleCompat);
146         mSubtitleView.setText("");
147         captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener());
148         updateWidestChar();
149     }
150 
getCaptionWindowId()151     public int getCaptionWindowId() {
152         return mCaptionWindowId;
153     }
154 
setCaptionWindowId(int captionWindowId)155     public void setCaptionWindowId(int captionWindowId) {
156         mCaptionWindowId = captionWindowId;
157     }
158 
clear()159     public void clear() {
160         clearText();
161         hide();
162     }
163 
show()164     public void show() {
165         setVisibility(View.VISIBLE);
166         requestLayout();
167     }
168 
hide()169     public void hide() {
170         setVisibility(View.INVISIBLE);
171         requestLayout();
172     }
173 
setPenAttr(CaptionPenAttr penAttr)174     public void setPenAttr(CaptionPenAttr penAttr) {
175         mCharacterStyles.clear();
176         if (penAttr.italic) {
177             mCharacterStyles.add(new StyleSpan(Typeface.ITALIC));
178         }
179         if (penAttr.underline) {
180             mCharacterStyles.add(new UnderlineSpan());
181         }
182         switch (penAttr.penSize) {
183             case CaptionPenAttr.PEN_SIZE_SMALL:
184                 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL));
185                 break;
186             case CaptionPenAttr.PEN_SIZE_LARGE:
187                 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE));
188                 break;
189         }
190         switch (penAttr.penOffset) {
191             case CaptionPenAttr.OFFSET_SUBSCRIPT:
192                 mCharacterStyles.add(new SubscriptSpan());
193                 break;
194             case CaptionPenAttr.OFFSET_SUPERSCRIPT:
195                 mCharacterStyles.add(new SuperscriptSpan());
196                 break;
197         }
198     }
199 
setPenColor(CaptionPenColor penColor)200     public void setPenColor(CaptionPenColor penColor) {
201         // TODO: apply pen colors or skip this and use the style of system wide cc style as is.
202     }
203 
setPenLocation(int row, int column)204     public void setPenLocation(int row, int column) {
205         // TODO: change the location of pen based on row and column both.
206         if (mRow >= 0) {
207             for (int r = mRow; r < row; ++r) {
208                 appendText("\n");
209             }
210         }
211         mRow = row;
212     }
213 
setWindowAttr(CaptionWindowAttr windowAttr)214     public void setWindowAttr(CaptionWindowAttr windowAttr) {
215         // TODO: apply window attrs or skip this and use the style of system wide cc style as is.
216     }
217 
sendBuffer(String buffer)218     public void sendBuffer(String buffer) {
219         appendText(buffer);
220     }
221 
sendControl(char control)222     public void sendControl(char control) {
223         // TODO: there are a bunch of ASCII-style control codes.
224     }
225 
226     /**
227      * This method places the window on a given CaptionLayout along with the anchor of the window.
228      * <p>
229      * According to CEA-708B, the anchor id indicates the gravity of the window as the follows.
230      * For example, A value 7 of a anchor id says that a window is align with its parent bottom and
231      * is located at the center horizontally of its parent.
232      * </p>
233      * <h4>Anchor id and the gravity of a window</h4>
234      * <table>
235      *     <tr>
236      *         <th>GRAVITY</th>
237      *         <th>LEFT</th>
238      *         <th>CENTER_HORIZONTAL</th>
239      *         <th>RIGHT</th>
240      *     </tr>
241      *     <tr>
242      *         <th>TOP</th>
243      *         <td>0</td>
244      *         <td>1</td>
245      *         <td>2</td>
246      *     </tr>
247      *     <tr>
248      *         <th>CENTER_VERTICAL</th>
249      *         <td>3</td>
250      *         <td>4</td>
251      *         <td>5</td>
252      *     </tr>
253      *     <tr>
254      *         <th>BOTTOM</th>
255      *         <td>6</td>
256      *         <td>7</td>
257      *         <td>8</td>
258      *     </tr>
259      * </table>
260      * <p>
261      * In order to handle the gravity of a window, there are two steps. First, set the size of the
262      * window. Since the window will be positioned at {@link ScaledLayout}, the size factors are
263      * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is
264      * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view,
265      * {@link SubtitleView}.
266      * </p>
267      * <p>
268      * The gravity of the window is also related to its size. When it should be pushed to a one of
269      * the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a boundary
270      * of the window. When it should be pushed in the horizontal/vertical center of its container,
271      * the horizontal/vertical center point of the window should be the same as the anchor point.
272      * </p>
273      *
274      * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area
275      * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the
276      *                      window
277      */
initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow)278     public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) {
279         if (DEBUG) {
280             Log.d(TAG, "initWindow with "
281                     + (captionLayout != null ? captionLayout.getCaptionTrack() : null));
282         }
283         if (mCaptionLayout != captionLayout) {
284             if (mCaptionLayout != null) {
285                 mCaptionLayout.removeOnLayoutChangeListener(this);
286             }
287             mCaptionLayout = captionLayout;
288             mCaptionLayout.addOnLayoutChangeListener(this);
289             updateWidestChar();
290         }
291 
292         // Both anchor vertical and horizontal indicates the position cell number of the window.
293         float scaleRow = (float) captionWindow.anchorVertical / (captionWindow.relativePositioning
294                 ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX);
295         float scaleCol = (float) captionWindow.anchorHorizontal /
296                 (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX
297                         : (isWideAspectRatio()
298                                 ? ANCHOR_HORIZONTAL_16_9_MAX : ANCHOR_HORIZONTAL_4_3_MAX));
299 
300         // The range of scaleRow/Col need to be verified to be in [0, 1].
301         // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}.
302         if (scaleRow < 0 || scaleRow > 1) {
303             Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 and 1"
304                     + " but " + scaleRow);
305             scaleRow = Math.max(0, Math.min(scaleRow, 1));
306         }
307         if (scaleCol < 0 || scaleCol > 1) {
308             Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0 and"
309                     + " 1 but " + scaleCol);
310             scaleCol = Math.max(0, Math.min(scaleCol, 1));
311         }
312         int gravity = Gravity.CENTER;
313         int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER;
314         int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER;
315         float scaleStartRow = 0;
316         float scaleEndRow = 1;
317         float scaleStartCol = 0;
318         float scaleEndCol = 1;
319         switch (horizontalMode) {
320             case ANCHOR_HORIZONTAL_MODE_LEFT:
321                 gravity = Gravity.LEFT;
322                 mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
323                 scaleStartCol = scaleCol;
324                 break;
325             case ANCHOR_HORIZONTAL_MODE_CENTER:
326                 float gap = Math.min(1 - scaleCol, scaleCol);
327 
328                 // Since all TV sets use left text alignment instead of center text alignment
329                 // for this case, we follow the industry convention if possible.
330                 int columnCount = captionWindow.columnCount + 1;
331                 if (isKoreanLanguageTrack()) {
332                     columnCount /= 2;
333                 }
334                 columnCount = Math.min(getScreenColumnCount(), columnCount);
335                 StringBuilder widestTextBuilder = new StringBuilder();
336                 for (int i = 0; i < columnCount; ++i) {
337                     widestTextBuilder.append(mWidestChar);
338                 }
339                 Paint paint = new Paint();
340                 paint.setTypeface(mCaptionStyleCompat.typeface);
341                 paint.setTextSize(mTextSize);
342                 float maxWindowWidth = paint.measureText(widestTextBuilder.toString());
343                 float halfMaxWidthScale = mCaptionLayout.getWidth() > 0
344                         ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f) : 0.0f;
345                 if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) {
346                     // Calculate the expected max window size based on the column count of the
347                     // caption window multiplied by average alphabets char width, then align the
348                     // left side of the window with the left side of the expected max window.
349                     gravity = Gravity.LEFT;
350                     mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL);
351                     scaleStartCol = scaleCol - halfMaxWidthScale;
352                     scaleEndCol = 1.0f;
353                 } else {
354                     // The gap will be the minimum distance value of the distances from both
355                     // horizontal end points to the anchor point.
356                     // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2].
357                     // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1].
358                     // The anchor point is located at the horizontal center of the window in both
359                     // cases.
360                     gravity = Gravity.CENTER_HORIZONTAL;
361                     mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
362                     scaleStartCol = scaleCol - gap;
363                     scaleEndCol = scaleCol + gap;
364                 }
365                 break;
366             case ANCHOR_HORIZONTAL_MODE_RIGHT:
367                 gravity = Gravity.RIGHT;
368                 mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE);
369                 scaleEndCol = scaleCol;
370                 break;
371         }
372         switch (verticalMode) {
373             case ANCHOR_VERTICAL_MODE_TOP:
374                 gravity |= Gravity.TOP;
375                 scaleStartRow = scaleRow;
376                 break;
377             case ANCHOR_VERTICAL_MODE_CENTER:
378                 gravity |= Gravity.CENTER_VERTICAL;
379 
380                 // See the above comment.
381                 float gap = Math.min(1 - scaleRow, scaleRow);
382                 scaleStartRow = scaleRow - gap;
383                 scaleEndRow = scaleRow + gap;
384                 break;
385             case ANCHOR_VERTICAL_MODE_BOTTOM:
386                 gravity |= Gravity.BOTTOM;
387                 scaleEndRow = scaleRow;
388                 break;
389         }
390         mCaptionLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout
391                 .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol));
392         setCaptionWindowId(captionWindow.id);
393         setRowLimit(captionWindow.rowCount);
394         setGravity(gravity);
395         if (captionWindow.visible) {
396             show();
397         } else {
398             hide();
399         }
400     }
401 
402     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)403     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
404             int oldTop, int oldRight, int oldBottom) {
405         int width = right - left;
406         int height = bottom - top;
407         if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) {
408             mLastCaptionLayoutWidth = width;
409             mLastCaptionLayoutHeight = height;
410             updateTextSize();
411         }
412     }
413 
isKoreanLanguageTrack()414     private boolean isKoreanLanguageTrack() {
415         return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null
416                 && mCaptionLayout.getCaptionTrack().language != null
417                 && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0;
418     }
419 
isWideAspectRatio()420     private boolean isWideAspectRatio() {
421         return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null
422                 && mCaptionLayout.getCaptionTrack().wideAspectRatio;
423     }
424 
updateWidestChar()425     private void updateWidestChar() {
426         if (isKoreanLanguageTrack()) {
427             mWidestChar = KOR_ALPHABET;
428         } else {
429             Paint paint = new Paint();
430             paint.setTypeface(mCaptionStyleCompat.typeface);
431             Charset latin1 = Charset.forName("ISO-8859-1");
432             float widestCharWidth = 0f;
433             for (int i = 0; i < 256; ++i) {
434                 String ch = new String(new byte[]{(byte) i}, latin1);
435                 float charWidth = paint.measureText(ch);
436                 if (widestCharWidth < charWidth) {
437                     widestCharWidth = charWidth;
438                     mWidestChar = ch;
439                 }
440             }
441         }
442         updateTextSize();
443     }
444 
updateTextSize()445     private void updateTextSize() {
446         if (mCaptionLayout == null) return;
447 
448         // Calculate text size based on the max window size.
449         StringBuilder widestTextBuilder = new StringBuilder();
450         int screenColumnCount = getScreenColumnCount();
451         for (int i = 0; i < screenColumnCount; ++i) {
452             widestTextBuilder.append(mWidestChar);
453         }
454         String widestText = widestTextBuilder.toString();
455         Paint paint = new Paint();
456         paint.setTypeface(mCaptionStyleCompat.typeface);
457         float startFontSize = 0f;
458         float endFontSize = 255f;
459         while (startFontSize < endFontSize) {
460             float testTextSize = (startFontSize + endFontSize) / 2f;
461             paint.setTextSize(testTextSize);
462             float width = paint.measureText(widestText);
463             if (mCaptionLayout.getWidth() * 0.8f > width) {
464                 startFontSize = testTextSize + 0.01f;
465             } else {
466                 endFontSize = testTextSize - 0.01f;
467             }
468         }
469         mTextSize = endFontSize * mFontScale;
470         mSubtitleView.setTextSize(mTextSize);
471     }
472 
getScreenColumnCount()473     private int getScreenColumnCount() {
474         float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight();
475         boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD;
476        if (isKoreanLanguageTrack()) {
477             // Each korean character consumes two slots.
478             if (isWideAspectRationScreen || isWideAspectRatio()) {
479                 return KR_MAX_COLUMN_COUNT_16_9 / 2;
480             } else {
481                 return KR_MAX_COLUMN_COUNT_4_3 / 2;
482             }
483         } else {
484             if (isWideAspectRationScreen || isWideAspectRatio()) {
485                 return US_MAX_COLUMN_COUNT_16_9;
486             } else {
487                 return US_MAX_COLUMN_COUNT_4_3;
488             }
489         }
490     }
491 
removeFromCaptionView()492     public void removeFromCaptionView() {
493         if (mCaptionLayout != null) {
494             mCaptionLayout.removeViewFromSafeTitleArea(this);
495             mCaptionLayout.removeOnLayoutChangeListener(this);
496             mCaptionLayout = null;
497         }
498     }
499 
setText(String text)500     public void setText(String text) {
501         updateText(text, false);
502     }
503 
appendText(String text)504     public void appendText(String text) {
505         updateText(text, true);
506     }
507 
clearText()508     public void clearText() {
509         mBuilder.clear();
510         mSubtitleView.setText("");
511     }
512 
updateText(String text, boolean appended)513     private void updateText(String text, boolean appended) {
514         if (!appended) {
515             mBuilder.clear();
516         }
517         if (text != null && text.length() > 0) {
518             int length = mBuilder.length();
519             mBuilder.append(text);
520             for (CharacterStyle characterStyle : mCharacterStyles) {
521                 mBuilder.setSpan(characterStyle, length, mBuilder.length(),
522                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
523             }
524         }
525         String[] lines = TextUtils.split(mBuilder.toString(), "\n");
526 
527         // Truncate text not to exceed the row limit.
528         // Plus one here since the range of the rows is [0, mRowLimit].
529         String truncatedText = TextUtils.join("\n", Arrays.copyOfRange(
530                 lines, Math.max(0, lines.length - (mRowLimit + 1)), lines.length));
531         mBuilder.delete(0, mBuilder.length() - truncatedText.length());
532 
533         // Trim the buffer first then set text to {@link SubtitleView}.
534         int start = 0, last = mBuilder.length() - 1;
535         int end = last;
536         while ((start <= end) && (mBuilder.charAt(start) <= ' ')) {
537             ++start;
538         }
539         while ((end >= start) && (mBuilder.charAt(end) <= ' ')) {
540             --end;
541         }
542         if (start == 0 && end == last) {
543             mSubtitleView.setText(mBuilder);
544         } else {
545             SpannableStringBuilder trim = new SpannableStringBuilder();
546             trim.append(mBuilder);
547             if (end < last) {
548                 trim.delete(end + 1, last + 1);
549             }
550             if (start > 0) {
551                 trim.delete(0, start);
552             }
553             mSubtitleView.setText(trim);
554         }
555     }
556 
setRowLimit(int rowLimit)557     public void setRowLimit(int rowLimit) {
558         if (rowLimit < 0) {
559             throw new IllegalArgumentException("A rowLimit should have a positive number");
560         }
561         mRowLimit = rowLimit;
562     }
563 }
564