ChannelBannerView.java revision 4e5a6a47adb40039a27536236656204f9ad12df3
1/*
2 * Copyright (C) 2014 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 com.android.tv.ui;
18
19import android.content.Context;
20import android.database.ContentObserver;
21import android.graphics.Bitmap;
22import android.graphics.Paint;
23import android.graphics.Typeface;
24import android.graphics.drawable.Drawable;
25import android.media.tv.TvContentRating;
26import android.media.tv.TvContract;
27import android.media.tv.TvInputInfo;
28import android.media.tv.TvInputManager;
29import android.net.Uri;
30import android.os.Handler;
31import android.text.TextUtils;
32import android.text.format.DateFormat;
33import android.util.AttributeSet;
34import android.util.LruCache;
35import android.util.TypedValue;
36import android.view.View;
37import android.widget.ImageView;
38import android.widget.ProgressBar;
39import android.widget.RelativeLayout;
40import android.widget.TextView;
41
42import com.android.tv.R;
43import com.android.tv.data.Channel;
44import com.android.tv.data.ChannelMap;
45import com.android.tv.data.Program;
46import com.android.tv.data.StreamInfo;
47import com.android.tv.util.Utils;
48
49/**
50 * A view to render channel banner.
51 */
52public class ChannelBannerView extends RelativeLayout implements Channel.LoadLogoCallback {
53    private static final int CACHE_SIZE = 10;
54    private TextView mChannelTextView;
55    private ImageView mChannelLogoImageView;
56    private TextView mProgramTextView;
57    private ImageView mTvInputLogoImageView;
58    private TextView mChannelNameTextView;
59    private TextView mProgramTimeTextView;
60    private ProgressBar mRemainingTimeView;
61    private TextView mVideoStatus;
62    // TODO: Need to get UX design for how to show ratings in the channel banner.
63    private TextView mRatingTextView;
64    private TextView mClosedCaptionTextView;
65    private TextView mAspectRatioTextView;
66    private TextView mResolutionTextView;
67    private TextView mAudioChannelTextView;
68    private TextView mProgrameDescriptionTextView;
69    private View mAnchorView;
70    private Uri mCurrentChannelUri;
71    private final LruCache<TvInputInfo, Drawable> mChannelInfoLogoCache =
72            new LruCache<TvInputInfo, Drawable> (CACHE_SIZE) {
73                @Override
74                protected Drawable create(TvInputInfo info) {
75                    return info.loadIcon(getContext());
76                }
77            };
78
79    private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) {
80        @Override
81        public void onChange(boolean selfChange, Uri uri) {
82            // TODO: This {@code uri} argument may be a program which is not related to this
83            // channel. Consider adding channel id as a parameter of program URI to avoid
84            // unnecessary update.
85            post(mProgramUpdateRunnable);
86        }
87    };
88
89    private final Runnable mProgramUpdateRunnable = new Runnable() {
90        @Override
91        public void run() {
92            removeCallbacks(this);
93            updateProgramInfo();
94        }
95    };
96
97    public ChannelBannerView(Context context) {
98        this(context, null);
99    }
100
101    public ChannelBannerView(Context context, AttributeSet attrs) {
102        super(context, attrs, 0);
103    }
104
105    public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
106        super(context, attrs, defStyle);
107    }
108
109    @Override
110    protected void onAttachedToWindow() {
111        super.onAttachedToWindow();
112        getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI,
113                true, mProgramUpdateObserver);
114    }
115
116    @Override
117    protected void onDetachedFromWindow() {
118        getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
119        super.onDetachedFromWindow();
120    }
121
122    @Override
123    protected void onFinishInflate() {
124        super.onFinishInflate();
125
126        mChannelTextView = (TextView) findViewById(R.id.channel_text);
127        mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo);
128        mProgramTextView = (TextView) findViewById(R.id.program_text);
129        mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo);
130        mChannelNameTextView = (TextView) findViewById(R.id.channel_name);
131        mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text);
132        mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time);
133        mVideoStatus = (TextView) findViewById(R.id.video_status);
134        mRatingTextView = (TextView) findViewById(R.id.rating);
135        mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption);
136        mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
137        mResolutionTextView = (TextView) findViewById(R.id.resolution);
138        mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
139        mProgrameDescriptionTextView = (TextView) findViewById(R.id.program_description);
140        mAnchorView = findViewById(R.id.anchor);
141    }
142
143    public void updateViews(ChannelMap channelMap, StreamInfo info) {
144        if (channelMap == null || !channelMap.isLoadFinished()) {
145            return;
146        }
147
148        String displayNumber = channelMap.getCurrentDisplayNumber();
149        if (displayNumber == null) {
150            displayNumber = "";
151        }
152
153        if (displayNumber.length() <= 3) {
154            updateTextView(
155                    mChannelTextView,
156                    R.dimen.channel_banner_title_large_text_size,
157                    R.dimen.channel_banner_title_large_margin_top);
158        } else if (displayNumber.length() <= 4) {
159            updateTextView(
160                    mChannelTextView,
161                    R.dimen.channel_banner_title_medium_text_size,
162                    R.dimen.channel_banner_title_medium_margin_top);
163        } else {
164            updateTextView(
165                    mChannelTextView,
166                    R.dimen.channel_banner_title_small_text_size,
167                    R.dimen.channel_banner_title_small_margin_top);
168        }
169        mChannelTextView.setText(displayNumber);
170
171        TvInputInfo inputInfo = (info == null) ? null : info.getCurrentTvInputInfo();
172        Drawable tvInputLogo = (inputInfo == null) ? null : mChannelInfoLogoCache.get(inputInfo);
173        if (tvInputLogo != null) {
174            mTvInputLogoImageView.setVisibility(View.VISIBLE);
175            mTvInputLogoImageView.setImageDrawable(tvInputLogo);
176        } else {
177            mTvInputLogoImageView.setVisibility(View.GONE);
178        }
179
180        String displayName = channelMap.getCurrentDisplayName();
181        if (displayName == null) {
182            displayName = "";
183        }
184        mChannelNameTextView.setText(displayName);
185
186        int resId = 0;
187        if (!info.isVideoAvailable()) {
188            switch (info.getVideoUnavailableReason()) {
189                case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNE:
190                    // We don't need to tell we're tuning.
191                    break;
192                case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
193                    resId = R.string.channel_banner_video_unavailable_weak_signal;
194                    break;
195                case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
196                    resId = R.string.channel_banner_video_unavailable_buffering;
197                    break;
198                default:
199                    resId = R.string.channel_banner_video_unavailable_unknown;
200                    break;
201            }
202        }
203        updateText(mVideoStatus, (resId == 0) ? null : getContext().getString(resId));
204
205        updateText(mClosedCaptionTextView, info.hasClosedCaption() ? "CC" : "");
206
207        updateText(mAspectRatioTextView,
208                Utils.getAspectRatioString(info.getVideoWidth(), info.getVideoHeight()));
209
210        updateText(mResolutionTextView,
211                Utils.getVideoDefinitionLevelString(info.getVideoDefinitionLevel()));
212
213        updateText(mAudioChannelTextView,
214                Utils.getAudioChannelString(info.getAudioChannelCount()));
215
216        mCurrentChannelUri = channelMap.getCurrentChannelUri();
217        if (channelMap.getCurrentChannel() != null) {
218            channelMap.getCurrentChannel().loadLogo(getContext(), this);
219        }
220
221        updateProgramInfo();
222    }
223
224    private void updateText(TextView view, String text) {
225        if (TextUtils.isEmpty(text)) {
226            view.setVisibility(View.GONE);
227        } else {
228            view.setVisibility(View.VISIBLE);
229            view.setText(text);
230        }
231    }
232
233    private void updateTextView(TextView textView, int sizeRes, int marginTopRes) {
234        float textSize = getContext().getResources().getDimension(sizeRes);
235        if (textView.getTextSize() != textSize) {
236            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
237        }
238        updateTopMargin(textView, marginTopRes);
239    }
240
241    private void updateTopMargin(View view, int marginTopRes) {
242        LayoutParams lp = (LayoutParams) view.getLayoutParams();
243        int topMargin = (int) getContext().getResources().getDimension(marginTopRes);
244        if (lp.topMargin != topMargin) {
245            lp.topMargin = topMargin;
246            view.setLayoutParams(lp);
247        }
248    }
249
250    @Override
251    public void onLoadLogoFinished(Channel channel, Bitmap logo) {
252        if (logo == null) {
253            mChannelLogoImageView.setVisibility(View.GONE);
254        } else {
255            mChannelLogoImageView.setImageBitmap(logo);
256            mChannelLogoImageView.setVisibility(View.VISIBLE);
257        }
258    }
259
260    private String getFormattedTimeString(long time) {
261        return DateFormat.format(
262                getContext().getString(R.string.channel_banner_time_format), time).toString();
263    }
264
265    private void updateProgramInfo() {
266        if (mCurrentChannelUri == null) {
267            handleNoProgramInformation();
268            return;
269        }
270
271        Program program = Utils.getCurrentProgram(getContext(), mCurrentChannelUri);
272        if (program == null) {
273            handleNoProgramInformation();
274            return;
275        }
276
277        String title = program.getTitle();
278        if (!TextUtils.isEmpty(title)) {
279            int width = mProgramTextView.getWidth();
280            if (width == 0) {
281                post(mProgramUpdateRunnable);
282            }
283            float largeTextSize = getContext().getResources().getDimension(
284                    R.dimen.channel_banner_program_large_text_size);
285            Typeface font = mProgramTextView.getTypeface();
286            int estimatedLineCount = estimateLineCount(title, font, largeTextSize, width);
287            boolean oneline = true;
288            if (estimatedLineCount > 1) {
289                updateTextView(
290                        mProgramTextView,
291                        R.dimen.channel_banner_program_medium_text_size,
292                        R.dimen.channel_banner_program_medium_margin_top);
293                float mediumTextSize = getContext().getResources().getDimension(
294                        R.dimen.channel_banner_program_medium_text_size);
295                if (estimateLineCount(title, font, mediumTextSize, width) > 1) {
296                    oneline = false;
297                }
298            } else {
299                updateTextView(
300                        mProgramTextView,
301                        R.dimen.channel_banner_program_large_text_size,
302                        R.dimen.channel_banner_program_large_margin_top);
303            }
304            updateTopMargin(mAnchorView, oneline
305                    ? R.dimen.channel_banner_anchor_one_line_y
306                    : R.dimen.channel_banner_anchor_two_line_y);
307            mProgramTextView.setText(title);
308
309            long startTime = program.getStartTimeUtcMillis();
310            long endTime = program.getEndTimeUtcMillis();
311            if (startTime > 0 && endTime > 0) {
312                mProgramTimeTextView.setVisibility(View.VISIBLE);
313                mRemainingTimeView.setVisibility(View.VISIBLE);
314
315                String startTimeText = getFormattedTimeString(startTime);
316                String endTimeText = getFormattedTimeString(endTime);
317
318                mProgramTimeTextView.setText(getContext().getString(
319                        R.string.channel_banner_program_time_format, startTimeText, endTimeText));
320
321                long currTime = System.currentTimeMillis();
322                if (currTime <= startTime) {
323                    mRemainingTimeView.setProgress(0);
324                } else if (currTime >= endTime) {
325                    mRemainingTimeView.setProgress(100);
326                } else {
327                    mRemainingTimeView.setProgress(
328                            (int) (100 *(currTime - startTime) / (endTime - startTime)));
329                }
330            } else {
331                mProgramTimeTextView.setVisibility(View.GONE);
332                mRemainingTimeView.setVisibility(View.GONE);
333            }
334        } else {
335            mProgramTextView.setText(getContext().getString(R.string.channel_banner_no_title));
336            mProgramTimeTextView.setVisibility(View.GONE);
337            mRemainingTimeView.setVisibility(View.GONE);
338        }
339
340        updateText(mRatingTextView, Utils.contentRatingsToString(program.getContentRatings()));
341
342        if (!TextUtils.isEmpty(program.getDescription())) {
343            mProgrameDescriptionTextView.setVisibility(View.VISIBLE);
344            mProgrameDescriptionTextView.setText(program.getDescription());
345        } else {
346            mProgrameDescriptionTextView.setVisibility(View.GONE);
347        }
348    }
349
350    private void handleNoProgramInformation() {
351        mProgramTextView.setText(getContext().getString(R.string.channel_banner_no_title));
352        mProgramTimeTextView.setVisibility(View.GONE);
353        mRemainingTimeView.setVisibility(View.GONE);
354        mProgrameDescriptionTextView.setVisibility(View.GONE);
355    }
356
357    private int estimateLineCount(String str, Typeface font, float textSize, int width) {
358        if (width == 0) {
359            return -1;
360        }
361        Paint paint = new Paint();
362        paint.setTypeface(font);
363        paint.setTextSize(textSize);
364        // Add +1 to measured size, because number of lines becomes 2
365        // when measured size equals width.
366        return divideRoundUp((int) paint.measureText(str) + 1, width);
367    }
368
369    private int divideRoundUp(int x, int y) {
370        return (x + y - 1) / y;
371    }
372}
373