blob: a736e79d90e8c32a5b8f4a06959e6ed4666c4ab7 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.PlaybackParams;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.TvViewSession;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.CommonConstants;
import com.android.tv.common.compat.TvInputConstantCompat;
import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.Debug;
import com.android.tv.common.util.DurationTimer;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.features.TvFeatures;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.ui.api.TunableTvViewPlayingApi;
import com.android.tv.util.NetworkUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageLoader;
import com.android.tv.common.flags.LegacyFlags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/** Includes the real {@link AppLayerTvView} handling tuning, block and other display events. */
public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvViewPlayingApi {
private static final boolean DEBUG = false;
private static final String TAG = "TunableTvView";
public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1;
public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
private final AccessibilityManager mAccessibilityManager;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
public @interface BlockScreenType {}
public static final int BLOCK_SCREEN_TYPE_NO_UI = 0;
public static final int BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW = 1;
public static final int BLOCK_SCREEN_TYPE_NORMAL = 2;
private static final String PERMISSION_RECEIVE_INPUT_EVENT =
CommonConstants.BASE_PACKAGE + ".permission.RECEIVE_INPUT_EVENT";
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TIME_SHIFT_STATE_NONE,
TIME_SHIFT_STATE_PLAY,
TIME_SHIFT_STATE_PAUSE,
TIME_SHIFT_STATE_REWIND,
TIME_SHIFT_STATE_FAST_FORWARD
})
private @interface TimeShiftState {}
private static final int TIME_SHIFT_STATE_NONE = 0;
private static final int TIME_SHIFT_STATE_PLAY = 1;
private static final int TIME_SHIFT_STATE_PAUSE = 2;
private static final int TIME_SHIFT_STATE_REWIND = 3;
private static final int TIME_SHIFT_STATE_FAST_FORWARD = 4;
private static final int FADED_IN = 0;
private static final int FADED_OUT = 1;
private static final int FADING_IN = 2;
private static final int FADING_OUT = 3;
private AppLayerTvView mTvView;
private TvViewSession mTvViewSession;
@Nullable private Channel mCurrentChannel;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
private ParentalControlSettings mParentalControlSettings;
private ProgramDataManager mProgramDataManager;
@Nullable private WatchedHistoryManager mWatchedHistoryManager;
private boolean mStarted;
private String mTagetInputId;
private TvInputInfo mInputInfo;
private OnTuneListener mOnTuneListener;
private int mVideoWidth;
private int mVideoHeight;
private int mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
private float mVideoFrameRate;
private float mVideoDisplayAspectRatio;
private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
private boolean mHasClosedCaption = false;
private boolean mScreenBlocked;
private OnScreenBlockingChangedListener mOnScreenBlockedListener;
private TvContentRating mBlockedContentRating;
private int mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
private boolean mCanReceiveInputEvent;
private boolean mIsMuted;
private float mVolume;
private boolean mParentControlEnabled;
private int mFixedSurfaceWidth;
private int mFixedSurfaceHeight;
private final boolean mCanModifyParentalControls;
private boolean mIsUnderShrunken;
@TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
private TimeShiftListener mTimeShiftListener;
private boolean mTimeShiftAvailable;
private long mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
private final Tracker mTracker;
private final DurationTimer mChannelViewTimer = new DurationTimer();
private InternetCheckTask mInternetCheckTask;
// A block screen view to hide the real TV view underlying. It may be used to enforce parental
// control, or hide screen when there's no video available and show appropriate information.
private final BlockScreenView mBlockScreenView;
private final int mTuningImageColorFilter;
// A spinner view to show buffering status.
private final View mBufferingSpinnerView;
private final View mDimScreenView;
private int mFadeState = FADED_IN;
private Runnable mActionAfterFade;
@BlockScreenType private int mBlockScreenType;
private final TvInputManagerHelper mInputManager;
private final ConnectivityManager mConnectivityManager;
private final InputSessionManager mInputSessionManager;
private int mChannelSignalStrength;
private final TvInputCallbackCompat mCallback =
new TvInputCallbackCompat() {
@Override
public void onConnectionFailed(String inputId) {
Log.w(TAG, "Failed to bind an input");
mTracker.sendInputConnectionFailure(inputId);
Channel channel = mCurrentChannel;
mCurrentChannel = null;
mInputInfo = null;
mCanReceiveInputEvent = false;
if (mOnTuneListener != null) {
// If tune is called inside onTuneFailed, mOnTuneListener will be set to
// a new instance. In order to avoid to clear the new mOnTuneListener,
// we copy mOnTuneListener to l and clear mOnTuneListener before
// calling onTuneFailed.
OnTuneListener listener = mOnTuneListener;
mOnTuneListener = null;
listener.onTuneFailed(channel);
}
}
@Override
public void onDisconnected(String inputId) {
Log.w(TAG, "Session is released by crash");
mTracker.sendInputDisconnected(inputId);
Channel channel = mCurrentChannel;
mCurrentChannel = null;
mInputInfo = null;
mCanReceiveInputEvent = false;
if (mOnTuneListener != null) {
OnTuneListener listener = mOnTuneListener;
mOnTuneListener = null;
listener.onUnexpectedStop(channel);
}
}
@Override
public void onChannelRetuned(String inputId, Uri channelUri) {
if (DEBUG) {
Log.d(
TAG,
"onChannelRetuned(inputId="
+ inputId
+ ", channelUri="
+ channelUri
+ ")");
}
if (mOnTuneListener != null) {
mOnTuneListener.onChannelRetuned(channelUri);
}
}
@Override
public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
mHasClosedCaption = false;
for (TvTrackInfo track : tracks) {
if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
mHasClosedCaption = true;
break;
}
}
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
@Override
public void onTrackSelected(String inputId, int type, String trackId) {
if (trackId == null) {
// A track is unselected.
if (type == TvTrackInfo.TYPE_VIDEO) {
mVideoWidth = 0;
mVideoHeight = 0;
mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
mVideoFrameRate = 0f;
mVideoDisplayAspectRatio = 0f;
} else if (type == TvTrackInfo.TYPE_AUDIO) {
mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
}
} else {
List<TvTrackInfo> tracks = getTracks(type);
boolean trackFound = false;
if (tracks != null) {
for (TvTrackInfo track : tracks) {
if (track.getId().equals(trackId)) {
if (type == TvTrackInfo.TYPE_VIDEO) {
mVideoWidth = track.getVideoWidth();
mVideoHeight = track.getVideoHeight();
mVideoFormat =
Utils.getVideoDefinitionLevelFromSize(
mVideoWidth, mVideoHeight);
mVideoFrameRate = track.getVideoFrameRate();
if (mVideoWidth <= 0 || mVideoHeight <= 0) {
mVideoDisplayAspectRatio = 0.0f;
} else {
float videoPixelAspectRatio =
track.getVideoPixelAspectRatio();
mVideoDisplayAspectRatio = (float) mVideoWidth
/ mVideoHeight;
mVideoDisplayAspectRatio *= videoPixelAspectRatio > 0 ?
videoPixelAspectRatio : 1;
}
} else if (type == TvTrackInfo.TYPE_AUDIO) {
mAudioChannelCount = track.getAudioChannelCount();
}
trackFound = true;
break;
}
}
}
if (!trackFound) {
Log.w(TAG, "Invalid track ID: " + trackId);
}
}
if (mOnTuneListener != null) {
// should not change audio track automatically when an audio track or a
// subtitle track is selected
mOnTuneListener.onStreamInfoChanged(
TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO);
}
}
@Override
public void onVideoAvailable(String inputId) {
if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}");
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log(
"Start up of TV app ends,"
+ " TunableTvView.onVideoAvailable resets timer");
Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
Debug.removeTimer(Debug.TAG_START_UP_TIMER);
mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
}
@Override
public void onVideoUnavailable(String inputId, int reason) {
if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
&& reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log(
"TunableTvView.onVideoUnAvailable reason = ("
+ reason
+ ") and removes timer");
Debug.removeTimer(Debug.TAG_START_UP_TIMER);
} else {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log("TunableTvView.onVideoUnAvailable reason = (" + reason + ")");
}
mVideoUnavailableReason = reason;
if (closePipIfNeeded()) {
return;
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
}
switch (reason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
break;
default:
// do nothing
}
}
@Override
public void onContentAllowed(String inputId) {
mBlockedContentRating = null;
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onContentAllowed();
}
}
@Override
public void onContentBlocked(String inputId, TvContentRating rating) {
if (rating != null && rating.equals(mBlockedContentRating)) {
return;
}
mBlockedContentRating = rating;
if (closePipIfNeeded()) {
return;
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onContentBlocked();
}
}
@Override
public void onTimeShiftStatusChanged(String inputId, int status) {
if (DEBUG) {
Log.d(
TAG,
"onTimeShiftStatusChanged: {inputId="
+ inputId
+ ", status="
+ status
+ "}");
}
boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
setTimeShiftAvailable(available);
}
@Override
public void onSignalStrength(String inputId, int value) {
mChannelSignalStrength = value;
if (mOnTuneListener != null) {
mOnTuneListener.onChannelSignalStrength();
}
}
};
public TunableTvView(Context context) {
this(context, null);
}
public TunableTvView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TunableTvView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
inflate(getContext(), R.layout.tunable_tv_view, this);
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
if (CommonFeatures.DVR.isEnabled(context)) {
mInputSessionManager = tvSingletons.getInputSessionManager();
} else {
mInputSessionManager = null;
}
mInputManager = tvSingletons.getTvInputManagerHelper();
mConnectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
mTracker = tvSingletons.getTracker();
mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
mBlockScreenView.addInfoFadeInAnimationListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
adjustBlockScreenSpacingAndText();
}
});
mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
mTuningImageColorFilter =
getResources().getColor(R.color.tvview_block_image_color_filter, null);
mDimScreenView = findViewById(R.id.dim_screen);
mDimScreenView
.animate()
.setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mActionAfterFade != null) {
mActionAfterFade.run();
}
}
@Override
public void onAnimationCancel(Animator animation) {
if (mActionAfterFade != null) {
mActionAfterFade.run();
}
}
});
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
}
public void initialize(
ProgramDataManager programDataManager,
TvInputManagerHelper tvInputManagerHelper,
LegacyFlags mLegacyFlags) {
mTvView = findViewById(R.id.tv_view);
mTvView.setUseSecureSurface(!BuildConfig.ENG && !mLegacyFlags.enableDeveloperFeatures());
mProgramDataManager = programDataManager;
mInputManagerHelper = tvInputManagerHelper;
mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings();
if (mInputSessionManager != null) {
mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback);
} else {
mTvView.setCallback(mCallback);
}
}
public void start() {
mStarted = true;
}
/** Warms up the input to reduce the start time. */
public void warmUpInput(String inputId, Uri channelUri) {
if (!mStarted && inputId != null && channelUri != null) {
if (mTvViewSession != null) {
mTvViewSession.tune(inputId, channelUri);
} else {
mTvView.tune(inputId, channelUri);
}
mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
updateBlockScreenAndMuting();
}
}
public void stop() {
if (!mStarted) {
return;
}
mStarted = false;
if (mCurrentChannel != null) {
long duration = mChannelViewTimer.reset();
mTracker.sendChannelViewStop(mCurrentChannel, duration);
if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
mWatchedHistoryManager.logChannelViewStop(
mCurrentChannel, System.currentTimeMillis(), duration);
}
}
reset();
}
/** Releases the resources. */
public void release() {
if (mInputSessionManager != null) {
mInputSessionManager.releaseTvViewSession(mTvViewSession);
mTvViewSession = null;
}
}
/** Resets TV view. */
public void reset() {
resetInternal();
mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
updateBlockScreenAndMuting();
}
/** Resets TV view to acquire the recording session. */
public void resetByRecording() {
resetInternal();
}
private void resetInternal() {
if (mTvViewSession != null) {
mTvViewSession.reset();
} else {
mTvView.reset();
}
mCurrentChannel = null;
mInputInfo = null;
mCanReceiveInputEvent = false;
mOnTuneListener = null;
setTimeShiftAvailable(false);
}
public void setMain() {
if (PermissionUtils.hasChangeHdmiCecActiveSource(getContext())) {
mTvView.setMain();
}
}
public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
mWatchedHistoryManager = watchedHistoryManager;
}
/** Sets if the TunableTvView is under shrunken. */
public void setIsUnderShrunken(boolean isUnderShrunken) {
mIsUnderShrunken = isUnderShrunken;
}
public int getChannelSignalStrength() {
return mChannelSignalStrength;
}
public void resetChannelSignalStrength() {
mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
}
@Override
public boolean isPlaying() {
return mStarted;
}
/** Called when parental control is changed. */
public void onParentalControlChanged(boolean enabled) {
mParentControlEnabled = enabled;
if (!enabled) {
// Unblock screen immediately if parental control is turned off
updateBlockScreenAndMuting();
}
}
/**
* Tunes to a channel with the {@code channelId}.
*
* @param params extra data to send it to TIS and store the data in TIMS.
* @return false, if the TV input is not a proper state to tune to a channel. For example, if
* the state is disconnected or channelId doesn't exist, it returns false.
*/
public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) {
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo");
if (!mStarted) {
throw new IllegalStateException("TvView isn't started");
}
if (DEBUG) Log.d(TAG, "tuneTo " + channel);
TvInputInfo inputInfo = mInputManagerHelper.getTvInputInfo(channel.getInputId());
if (inputInfo == null) {
return false;
}
if (mCurrentChannel != null) {
long duration = mChannelViewTimer.reset();
mTracker.sendChannelViewStop(mCurrentChannel, duration);
if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
mWatchedHistoryManager.logChannelViewStop(
mCurrentChannel, System.currentTimeMillis(), duration);
}
}
mOnTuneListener = listener;
mCurrentChannel = channel;
boolean tunedByRecommendation =
params != null
&& params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE)
!= null;
boolean needSurfaceSizeUpdate = false;
if (!inputInfo.equals(mInputInfo)) {
mTagetInputId = inputInfo.getId();
mInputInfo = inputInfo;
mCanReceiveInputEvent =
getContext()
.getPackageManager()
.checkPermission(
PERMISSION_RECEIVE_INPUT_EVENT,
mInputInfo.getServiceInfo().packageName)
== PackageManager.PERMISSION_GRANTED;
if (DEBUG) {
Log.d(
TAG,
"Input \'"
+ mInputInfo.getId()
+ "\' can receive input event: "
+ mCanReceiveInputEvent);
}
needSurfaceSizeUpdate = true;
}
mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation);
mChannelViewTimer.start();
mVideoWidth = 0;
mVideoHeight = 0;
mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
mVideoFrameRate = 0f;
mVideoDisplayAspectRatio = 0f;
mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
mHasClosedCaption = false;
mBlockedContentRating = null;
mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
// To reduce the IPCs, unregister the callback here and register it when necessary.
mTvView.setTimeShiftPositionCallback(null);
setTimeShiftAvailable(false);
mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
if (mTvViewSession != null) {
mTvViewSession.tune(channel, params, listener);
} else {
mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
}
if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
// When the input is changed, TvView recreates its SurfaceView internally.
// So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
SurfaceView surfaceView = getSurfaceView();
if (surfaceView != null) {
surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
} else {
Log.w(TAG, "Failed to set fixed size for surface view: Null surface view");
}
}
updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(this, true);
}
return true;
}
@Override
@Nullable
public Channel getCurrentChannel() {
return mCurrentChannel;
}
/**
* Sets the current channel. Call this method only when setting the current channel without
* actually tuning to it.
*
* @param currentChannel The new current channel to set to.
*/
public void setCurrentChannel(Channel currentChannel) {
mCurrentChannel = currentChannel;
}
@Override
public void setStreamVolume(float volume) {
if (!mStarted) {
throw new IllegalStateException("TvView isn't started");
}
if (DEBUG) Log.d(TAG, "setStreamVolume " + volume);
mVolume = volume;
if (!mIsMuted) {
mTvView.setStreamVolume(volume);
}
}
/**
* Sets fixed size for the internal {@link android.view.Surface} of {@link
* android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the
* {@link android.view.Surface}'s size will be matched to the layout.
*
* <p>Note: Once {@link android.view.SurfaceHolder#setFixedSize} is called, {@link
* android.view.SurfaceView} and its underlying window can be misaligned, when the size of
* {@link android.view.SurfaceView} is changed without changing either left position or top
* position. For detail, please refer the codes of android.view.SurfaceView.updateWindow().
*/
public void setFixedSurfaceSize(int width, int height) {
mFixedSurfaceWidth = width;
mFixedSurfaceHeight = height;
if (mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
// When the input is changed, TvView recreates its SurfaceView internally.
// So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
} else {
SurfaceView surfaceView = (SurfaceView) mTvView.getChildAt(0);
surfaceView.getHolder().setSizeFromLayout();
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return mCanReceiveInputEvent && mTvView.dispatchKeyEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return mCanReceiveInputEvent && mTvView.dispatchTouchEvent(event);
}
@Override
public boolean dispatchTrackballEvent(MotionEvent event) {
return mCanReceiveInputEvent && mTvView.dispatchTrackballEvent(event);
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
return mCanReceiveInputEvent && mTvView.dispatchGenericMotionEvent(event);
}
public interface OnTuneListener {
void onTuneFailed(Channel channel);
void onUnexpectedStop(Channel channel);
void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack);
void onChannelRetuned(Uri channel);
void onContentBlocked();
void onContentAllowed();
void onChannelSignalStrength();
}
public void unblockContent(TvContentRating rating) {
mTvView.unblockContent(rating);
}
@Override
public int getVideoWidth() {
return mVideoWidth;
}
@Override
public int getVideoHeight() {
return mVideoHeight;
}
@Override
public int getVideoDefinitionLevel() {
return mVideoFormat;
}
@Override
public float getVideoFrameRate() {
return mVideoFrameRate;
}
/** Returns displayed aspect ratio (video width / video height * pixel ratio). */
@Override
public float getVideoDisplayAspectRatio() {
return mVideoDisplayAspectRatio;
}
@Override
public int getAudioChannelCount() {
return mAudioChannelCount;
}
@Override
public boolean hasClosedCaption() {
return mHasClosedCaption;
}
@Override
public boolean isVideoAvailable() {
return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE;
}
@Override
public boolean isVideoOrAudioAvailable() {
return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE
|| mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY;
}
@Override
public int getVideoUnavailableReason() {
return mVideoUnavailableReason;
}
/** Returns the {@link android.view.SurfaceView} of the {@link android.media.tv.TvView}. */
private SurfaceView getSurfaceView() {
return (SurfaceView) mTvView.getChildAt(0);
}
public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
mTvView.setOnUnhandledInputEventListener(listener);
}
public void setClosedCaptionEnabled(boolean enabled) {
mTvView.setCaptionEnabled(enabled);
}
@VisibleForTesting
public void setOnTuneListener(OnTuneListener listener) {
mOnTuneListener = listener;
}
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
public String getSelectedTrack(int type) {
return mTvView.getSelectedTrack(type);
}
public void selectTrack(int type, String trackId) {
mTvView.selectTrack(type, trackId);
}
/**
* Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView},
* which is the actual view to play live TV videos.
*/
public MarginLayoutParams getTvViewLayoutParams() {
return (MarginLayoutParams) mTvView.getLayoutParams();
}
/**
* Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying {@link TvView},
* which is the actual view to play live TV videos.
*/
public void setTvViewLayoutParams(MarginLayoutParams layoutParams) {
mTvView.setLayoutParams(layoutParams);
}
/**
* Gets the underlying {@link AppLayerTvView}, which is the actual view to play live TV videos.
*/
public TvView getTvView() {
return mTvView;
}
/**
* Returns if the screen is blocked, either by {@link #blockOrUnblockScreen(boolean)} or because
* the content is blocked.
*/
public boolean isBlocked() {
return isScreenBlocked() || isContentBlocked();
}
/** Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}. */
public boolean isScreenBlocked() {
return mScreenBlocked;
}
/** Returns {@code true} if the content is blocked, otherwise {@code false}. */
public boolean isContentBlocked() {
return mBlockedContentRating != null;
}
public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) {
mOnScreenBlockedListener = listener;
}
/** Returns currently blocked content rating. {@code null} if it's not blocked. */
@Override
public TvContentRating getBlockedContentRating() {
return mBlockedContentRating;
}
/**
* Blocks/unblocks current TV screen and mutes. There would be black screen with lock icon in
* order to show that screen block is intended and not an error.
*
* @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock.
*/
public void blockOrUnblockScreen(boolean blockOrUnblock) {
if (mScreenBlocked == blockOrUnblock) {
return;
}
mScreenBlocked = blockOrUnblock;
if (closePipIfNeeded()) {
return;
}
updateBlockScreenAndMuting();
if (mOnScreenBlockedListener != null) {
mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock);
}
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (mTvView != null) {
mTvView.setVisibility(visibility);
}
}
/**
* Set the type of block screen. If {@code type} is set to {@code BLOCK_SCREEN_TYPE_NO_UI}, the
* block screen will not show any description such as a lock icon and a text for the blocked
* reason, if {@code type} is set to {@code BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW}, the block
* screen will show the description for shrunken tv view (Small icon and short text), and if
* {@code type} is set to {@code BLOCK_SCREEN_TYPE_NORMAL}, the block screen will show the
* description for normal tv view (Big icon and long text).
*
* @param type The type of block screen to set.
*/
public void setBlockScreenType(@BlockScreenType int type) {
if (mBlockScreenType != type) {
mBlockScreenType = type;
updateBlockScreen(true);
}
}
private void updateBlockScreen(boolean animation) {
mBlockScreenView.endAnimations();
int blockReason =
(mScreenBlocked || mBlockedContentRating != null) && mParentControlEnabled
? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED
: mVideoUnavailableReason;
if (blockReason != VIDEO_UNAVAILABLE_REASON_NONE) {
mBufferingSpinnerView.setVisibility(
blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
|| blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
? VISIBLE
: GONE);
if (!animation) {
adjustBlockScreenSpacingAndText();
}
if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
return;
}
mBlockScreenView.setVisibility(VISIBLE);
mBlockScreenView.setBackgroundImage(null);
if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
mBlockScreenView.setIconVisibility(true);
if (!mCanModifyParentalControls) {
mBlockScreenView.setIconImage(R.drawable.ic_message_lock_no_permission);
mBlockScreenView.setIconScaleType(ImageView.ScaleType.CENTER);
} else {
mBlockScreenView.setIconImage(R.drawable.ic_message_lock);
mBlockScreenView.setIconScaleType(ImageView.ScaleType.FIT_CENTER);
}
} else {
if (mInternetCheckTask != null) {
mInternetCheckTask.cancel(true);
mInternetCheckTask = null;
}
mBlockScreenView.setIconVisibility(false);
if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) {
showImageForTuningIfNeeded();
} else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
&& mCurrentChannel != null
&& !mCurrentChannel.isPhysicalTunerChannel()) {
mInternetCheckTask = new InternetCheckTask();
mInternetCheckTask.execute();
}
}
mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
} else {
mBufferingSpinnerView.setVisibility(GONE);
if (mBlockScreenView.getVisibility() == VISIBLE) {
mBlockScreenView.fadeOut();
}
}
}
private void adjustBlockScreenSpacingAndText() {
mBlockScreenView.setSpacing(mBlockScreenType);
String text = getBlockScreenText();
if (text != null) {
mBlockScreenView.setInfoText(text);
}
mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
}
/**
* Returns the block screen text corresponding to the current status. Note that returning {@code
* null} value means that the current text should not be changed.
*/
private String getBlockScreenText() {
// TODO: add a test for this method
Resources res = getResources();
boolean isA11y = mAccessibilityManager.isEnabled();
if (mScreenBlocked && mParentControlEnabled) {
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
return "";
case BLOCK_SCREEN_TYPE_NORMAL:
if (mCanModifyParentalControls) {
return res.getString(
isA11y
? R.string.tvview_channel_locked_talkback
: R.string.tvview_channel_locked);
} else {
return res.getString(R.string.tvview_channel_locked_no_permission);
}
}
} else if (mBlockedContentRating != null && mParentControlEnabled) {
String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
return "";
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
if (TextUtils.isEmpty(name)) {
return res.getString(R.string.shrunken_tvview_content_locked);
} else if (name.equals(res.getString(R.string.unrated_rating_name))) {
return res.getString(R.string.shrunken_tvview_content_locked_unrated);
} else {
return res.getString(R.string.shrunken_tvview_content_locked_format, name);
}
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
if (mCanModifyParentalControls) {
return res.getString(
isA11y
? R.string.tvview_content_locked_talkback
: R.string.tvview_content_locked);
} else {
return res.getString(R.string.tvview_content_locked_no_permission);
}
} else {
if (mCanModifyParentalControls) {
return name.equals(res.getString(R.string.unrated_rating_name))
? res.getString(
isA11y
? R.string
.tvview_content_locked_unrated_talkback
: R.string.tvview_content_locked_unrated)
: res.getString(
isA11y
? R.string.tvview_content_locked_format_talkback
: R.string.tvview_content_locked_format,
name);
} else {
return name.equals(res.getString(R.string.unrated_rating_name))
? res.getString(
R.string.tvview_content_locked_unrated_no_permission)
: res.getString(
R.string.tvview_content_locked_format_no_permission,
name);
}
}
}
} else if (mVideoUnavailableReason != VIDEO_UNAVAILABLE_REASON_NONE) {
switch (mVideoUnavailableReason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
return res.getString(R.string.tvview_msg_audio_only);
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return res.getString(R.string.tvview_msg_weak_signal);
case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
return res.getString(R.string.msg_channel_unavailable_not_connected);
case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
return getTuneConflictMessage();
default:
return "";
}
}
return null;
}
private boolean closePipIfNeeded() {
if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(getContext())
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& ((Activity) getContext()).isInPictureInPictureMode()
&& (mScreenBlocked
|| mBlockedContentRating != null
|| mVideoUnavailableReason
== TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
|| mVideoUnavailableReason
== CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
((Activity) getContext()).finish();
return true;
}
return false;
}
private void updateBlockScreenAndMuting() {
updateBlockScreen(false);
updateMuteStatus();
}
private boolean shouldShowImageForTuning() {
if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
|| mScreenBlocked
|| mBlockedContentRating != null
|| mCurrentChannel == null
|| mIsUnderShrunken
|| getWidth() == 0
|| getWidth() == 0
|| !isBundledInput()) {
return false;
}
Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
if (currentProgram == null) {
return false;
}
TvContentRating rating =
mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings());
return !(mParentControlEnabled && rating != null);
}
private void showImageForTuningIfNeeded() {
if (shouldShowImageForTuning()) {
if (mCurrentChannel == null) {
return;
}
Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
if (currentProgram != null) {
currentProgram.loadPosterArt(
getContext(),
getWidth(),
getHeight(),
createProgramPosterArtCallback(mCurrentChannel.getId()));
}
}
}
private String getTuneConflictMessage() {
if (mTagetInputId != null) {
TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId);
Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId);
if (timeMs != null) {
return getResources()
.getQuantityString(
R.plurals.tvview_msg_input_no_resource,
input.getTunerCount(),
DateUtils.formatDateTime(
getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME));
}
}
return null;
}
private void updateMuteStatus() {
// Workaround: BaseTunerTvInputService uses AC3 pass-through implementation, which disables
// audio tracks to enforce the mute request. We don't want to send mute request if we are
// not going to block the screen to prevent the video jankiness resulted by disabling audio
// track before the playback is started. In other way, we should send unmute request before
// the playback is started, because TunerTvInput will remember the muted state and mute
// itself right way when the playback is going to be started, which results the initial
// jankiness, too.
boolean isBundledInput = isBundledInput();
if ((isBundledInput || isVideoOrAudioAvailable())
&& !mScreenBlocked
&& mBlockedContentRating == null) {
if (mIsMuted) {
mIsMuted = false;
mTvView.setStreamVolume(mVolume);
}
} else {
if (!mIsMuted) {
if ((mInputInfo == null || isBundledInput)
&& !mScreenBlocked
&& mBlockedContentRating == null) {
return;
}
mIsMuted = true;
mTvView.setStreamVolume(0);
}
}
}
private boolean isBundledInput() {
return mInputInfo != null
&& mInputInfo.getType() == TvInputInfo.TYPE_TUNER
&& CommonUtils.isBundledInput(mInputInfo.getId());
}
/** Returns true if this view is faded out. */
public boolean isFadedOut() {
return mFadeState == FADED_OUT;
}
/** Fade out this TunableTvView. Fade out by increasing the dimming. */
public void fadeOut(
int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) {
mDimScreenView.setAlpha(0f);
mDimScreenView.setVisibility(View.VISIBLE);
mDimScreenView
.animate()
.alpha(1f)
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
() -> {
mFadeState = FADING_OUT;
mActionAfterFade = actionAfterFade;
})
.withEndAction(() -> mFadeState = FADED_OUT);
}
/** Fade in this TunableTvView. Fade in by decreasing the dimming. */
public void fadeIn(
int durationMillis, TimeInterpolator interpolator, final Runnable actionAfterFade) {
mDimScreenView.setAlpha(1f);
mDimScreenView.setVisibility(View.VISIBLE);
mDimScreenView
.animate()
.alpha(0f)
.setDuration(durationMillis)
.setInterpolator(interpolator)
.withStartAction(
() -> {
mFadeState = FADING_IN;
mActionAfterFade = actionAfterFade;
})
.withEndAction(
() -> {
mFadeState = FADED_IN;
mDimScreenView.setVisibility(View.GONE);
});
}
/** Remove the fade effect. */
public void removeFadeEffect() {
mDimScreenView.animate().cancel();
mDimScreenView.setVisibility(View.GONE);
mFadeState = FADED_IN;
}
/**
* Sets the TimeShiftListener
*
* @param listener The instance of {@link TimeShiftListener}.
*/
@Override
public void setTimeShiftListener(TimeShiftListener listener) {
mTimeShiftListener = listener;
}
public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
mBlockScreenView.setInfoTextOnClickListener(onClickListener);
}
private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
if (mTimeShiftAvailable == isTimeShiftAvailable) {
return;
}
mTimeShiftAvailable = isTimeShiftAvailable;
if (isTimeShiftAvailable) {
mTvView.setTimeShiftPositionCallback(
new TvView.TimeShiftPositionCallback() {
@Override
public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
if (mTimeShiftListener != null
&& mCurrentChannel != null
&& mCurrentChannel.getInputId().equals(inputId)) {
mTimeShiftListener.onRecordStartTimeChanged(timeMs);
}
}
@Override
public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
mTimeShiftCurrentPositionMs = timeMs;
}
});
} else {
mTvView.setTimeShiftPositionCallback(null);
}
if (mTimeShiftListener != null) {
mTimeShiftListener.onAvailabilityChanged();
}
}
/** Returns if the time shift is available for the current channel. */
@Override
public boolean isTimeShiftAvailable() {
return mTimeShiftAvailable;
}
/** Plays the media, if the current input supports time-shifting. */
@Override
public void timeShiftPlay() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
if (mTimeShiftState == TIME_SHIFT_STATE_PLAY) {
return;
}
mTvView.timeShiftResume();
}
/** Pauses the media, if the current input supports time-shifting. */
@Override
public void timeShiftPause() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
if (mTimeShiftState == TIME_SHIFT_STATE_PAUSE) {
return;
}
mTvView.timeShiftPause();
}
/**
* Rewinds the media with the given speed, if the current input supports time-shifting.
*
* @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
public void timeShiftRewind(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
if (speed <= 0) {
throw new IllegalArgumentException("The speed should be a positive integer.");
}
mTimeShiftState = TIME_SHIFT_STATE_REWIND;
PlaybackParams params = new PlaybackParams();
params.setSpeed(speed * -1);
mTvView.timeShiftSetPlaybackParams(params);
}
}
/**
* Fast-forwards the media with the given speed, if the current input supports time-shifting.
*
* @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
*/
@Override
public void timeShiftFastForward(int speed) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
} else {
if (speed <= 0) {
throw new IllegalArgumentException("The speed should be a positive integer.");
}
mTimeShiftState = TIME_SHIFT_STATE_FAST_FORWARD;
PlaybackParams params = new PlaybackParams();
params.setSpeed(speed);
mTvView.timeShiftSetPlaybackParams(params);
}
}
/**
* Seek to the given time position.
*
* @param timeMs The time in milliseconds to seek to.
*/
@Override
public void timeShiftSeekTo(long timeMs) {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
mTvView.timeShiftSeekTo(timeMs);
}
/** Returns the current playback position in milliseconds. */
@Override
public long timeShiftGetCurrentPositionMs() {
if (!isTimeShiftAvailable()) {
throw new IllegalStateException("Time-shift is not supported for the current channel");
}
if (DEBUG) {
Log.d(
TAG,
"timeShiftGetCurrentPositionMs: current position ="
+ Utils.toTimeString(mTimeShiftCurrentPositionMs));
}
return mTimeShiftCurrentPositionMs;
}
private ImageLoader.ImageLoaderCallback<BlockScreenView> createProgramPosterArtCallback(
final long channelId) {
return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) {
@Override
public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) {
if (posterArt == null
|| getCurrentChannel() == null
|| channelId != getCurrentChannel().getId()
|| !shouldShowImageForTuning()) {
return;
}
Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt);
drawablePosterArt
.mutate()
.setColorFilter(mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER);
view.setBackgroundImage(drawablePosterArt);
}
};
}
/** A listener which receives the notification when the screen is blocked/unblocked. */
public abstract static class OnScreenBlockingChangedListener {
/** Called when the screen is blocked/unblocked. */
public abstract void onScreenBlockingChanged(boolean blocked);
}
private class InternetCheckTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
return NetworkUtils.isNetworkAvailable(mConnectivityManager);
}
@Override
protected void onPostExecute(Boolean networkAvailable) {
mInternetCheckTask = null;
if (!networkAvailable
&& isAttachedToWindow()
&& !mScreenBlocked
&& mBlockedContentRating == null
&& mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
mBlockScreenView.setIconVisibility(true);
mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud);
mBlockScreenView.setInfoText(R.string.tvview_msg_no_internet_connection);
}
}
}
}