blob: 78fda42ae718a7a250691469997819eed89800e0 [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;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.provider.Settings;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.MemoryManageable;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrPlayActivity;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.search.ProgramGuideSearchFragment;
import com.android.tv.ui.AppLayerTvView;
import com.android.tv.ui.ChannelBannerView;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
import com.android.tv.ui.OverlayRootView;
import com.android.tv.ui.SelectInputView;
import com.android.tv.ui.SelectInputView.OnInputSelectedCallback;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
import com.android.tv.ui.TvOverlayManager;
import com.android.tv.ui.TvViewUiManager;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.CustomizeChannelListFragment;
import com.android.tv.ui.sidepanel.DebugOptionFragment;
import com.android.tv.ui.sidepanel.DisplayModeFragment;
import com.android.tv.ui.sidepanel.MultiAudioFragment;
import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.util.CaptionSettings;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.PipInputManager;
import com.android.tv.util.PipInputManager.PipInput;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.SearchManagerHelper;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.PipSound;
import com.android.usbtuner.UsbTunerPreferences;
import com.android.usbtuner.setup.TunerSetupActivity;
import com.android.usbtuner.tvinput.UsbTunerTvInputService;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* The main activity for the Live TV app.
*/
public class MainActivity extends Activity implements AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
@Retention(RetentionPolicy.SOURCE)
@IntDef({KEY_EVENT_HANDLER_RESULT_PASSTHROUGH, KEY_EVENT_HANDLER_RESULT_NOT_HANDLED,
KEY_EVENT_HANDLER_RESULT_HANDLED, KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY})
public @interface KeyHandlerResultType {}
public static final int KEY_EVENT_HANDLER_RESULT_PASSTHROUGH = 0;
public static final int KEY_EVENT_HANDLER_RESULT_NOT_HANDLED = 1;
public static final int KEY_EVENT_HANDLER_RESULT_HANDLED = 2;
public static final int KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY = 3;
private static final boolean USE_BACK_KEY_LONG_PRESS = false;
private static final float AUDIO_MAX_VOLUME = 1.0f;
private static final float AUDIO_MIN_VOLUME = 0.0f;
private static final float AUDIO_DUCKING_VOLUME = 0.3f;
private static final float FRAME_RATE_FOR_FILM = 23.976f;
private static final float FRAME_RATE_EPSILON = 0.1f;
private static final float MEDIA_SESSION_STOPPED_SPEED = 0.0f;
private static final float MEDIA_SESSION_PLAYING_SPEED = 1.0f;
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
private static final String USB_TV_TUNER_INPUT_ID =
"com.android.tv/com.android.usbtuner.tvinput.UsbTunerTvInputService";
private static final String DVR_TEST_INPUT_ID = USB_TV_TUNER_INPUT_ID;
// Tracker screen names.
public static final String SCREEN_NAME = "Main";
private static final String SCREEN_BEHIND_NAME = "Behind";
private static final float REFRESH_RATE_EPSILON = 0.01f;
private static final HashSet<Integer> BLACKLIST_KEYCODE_TO_TIS;
// These keys won't be passed to TIS in addition to gamepad buttons.
static {
BLACKLIST_KEYCODE_TO_TIS = new HashSet<>();
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_TV_INPUT);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MENU);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_CHANNEL_UP);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_CHANNEL_DOWN);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_UP);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_DOWN);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_MUTE);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MUTE);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_SEARCH);
}
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
private static final int REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS = 2;
private static final String KEY_INIT_CHANNEL_ID = "com.android.tv.init_channel_id";
private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
// Change channels with key long press.
private static final int CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS = 3000;
private static final int CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED = 50;
private static final int CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED = 200;
private static final int CHANNEL_CHANGE_INITIAL_DELAY_MILLIS = 500;
private static final int FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS = 500;
private static final int MSG_CHANNEL_DOWN_PRESSED = 1000;
private static final int MSG_CHANNEL_UP_PRESSED = 1001;
private static final int MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE = 1002;
@Retention(RetentionPolicy.SOURCE)
@IntDef({UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE,
UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO,
UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK})
private @interface ChannelBannerUpdateReason {}
private static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1;
private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2;
private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3;
private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4;
private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000;
// Lazy initialization.
// Delay 1 second in order not to interrupt the first tune.
private static final long LAZY_INITIALIZATION_DELAY = TimeUnit.SECONDS.toMillis(1);
private AccessibilityManager mAccessibilityManager;
private ChannelDataManager mChannelDataManager;
private ProgramDataManager mProgramDataManager;
private TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
private PipInputManager mPipInputManager;
private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
private TvViewUiManager mTvViewUiManager;
private TimeShiftManager mTimeShiftManager;
private Tracker mTracker;
private final DurationTimer mMainDurationTimer = new DurationTimer();
private final DurationTimer mTuneDurationTimer = new DurationTimer();
private DvrManager mDvrManager;
private DvrDataManager mDvrDataManager;
private TunableTvView mTvView;
private TunableTvView mPipView;
private OverlayRootView mOverlayRootView;
private Bundle mTuneParams;
private boolean mChannelBannerHiddenBySideFragment;
// TODO: Move the scene views into TvTransitionManager or TvOverlayManager.
private ChannelBannerView mChannelBannerView;
private KeypadChannelSwitchView mKeypadChannelSwitchView;
@Nullable
private Uri mInitChannelUri;
@Nullable
private String mParentInputIdWhenScreenOff;
private boolean mScreenOffIntentReceived;
private boolean mShowProgramGuide;
private boolean mShowSelectInputView;
private TvInputInfo mInputToSetUp;
private final List<MemoryManageable> mMemoryManageables = new ArrayList<>();
private MediaSession mMediaSession;
private int mNowPlayingCardWidth;
private int mNowPlayingCardHeight;
private final MyOnTuneListener mOnTuneListener = new MyOnTuneListener();
private String mInputIdUnderSetup;
private boolean mIsSetupActivityCalledByPopup;
private AudioManager mAudioManager;
private int mAudioFocusStatus;
private boolean mTunePending;
private boolean mPipEnabled;
private Channel mPipChannel;
private boolean mPipSwap;
@PipSound private int mPipSound = TvSettings.PIP_SOUND_MAIN; // Default
private boolean mDebugNonFullSizeScreen;
private boolean mActivityResumed;
private boolean mActivityStarted;
private boolean mShouldTuneToTunerChannel;
private boolean mUseKeycodeBlacklist;
private boolean mShowLockedChannelsTemporarily;
private boolean mBackKeyPressed;
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
private boolean mAc3PassthroughSupported;
private boolean mShowNewSourcesFragment = true;
private Uri mRecordingUri;
private String mUsbTunerInputId;
private boolean mOtherActivityLaunched;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
private TvOverlayManager mOverlayManager;
// mIsCurrentChannelUnblockedByUser and mWasChannelUnblockedBeforeShrunkenByUser are used for
// keeping the channel unblocking status while TV view is shrunken.
private boolean mIsCurrentChannelUnblockedByUser;
private boolean mWasChannelUnblockedBeforeShrunkenByUser;
private Channel mChannelBeforeShrunkenTvView;
private Channel mPipChannelBeforeShrunkenTvView;
private boolean mIsCompletingShrunkenTvView;
// TODO: Need to consider the case that TIS explicitly request PIN code while TV view is
// shrunken.
private TvContentRating mLastAllowedRatingForCurrentChannel;
private TvContentRating mAllowedRatingBeforeShrunken;
private CaptionSettings mCaptionSettings;
// Lazy initialization
private boolean mLazyInitialized;
private static final int MAX_RECENT_CHANNELS = 5;
private final ArrayDeque<Long> mRecentChannels = new ArrayDeque<>(MAX_RECENT_CHANNELS);
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private RecurringRunner mSendConfigInfoRecurringRunner;
private RecurringRunner mChannelStatusRecurringRunner;
// A caller which started this activity. (e.g. TvSearch)
private String mSource;
private final Handler mHandler = new MainActivityHandler(this);
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_OFF");
// We need to stop TvView, when the screen is turned off. If not and TIS uses
// MediaPlayer, a device may not go to the sleep mode and audio can be heard,
// because MediaPlayer keeps playing media by its wake lock.
mScreenOffIntentReceived = true;
markCurrentChannelDuringScreenOff();
stopAll(true);
} else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON");
if (!mActivityResumed && mVisibleBehind) {
// ACTION_SCREEN_ON is usually called after onResume. But, if media is played
// under launcher with requestVisibleBehind(true), onResume will not be called.
// In this case, we need to resume TvView and PipView explicitly.
resumeTvIfNeeded();
resumePipIfNeeded();
}
} else if (intent.getAction().equals(
TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)) {
if (DEBUG) Log.d(TAG, "Received parental control settings change");
checkChannelLockNeeded(mTvView);
checkChannelLockNeeded(mPipView);
applyParentalControlSettings();
}
}
};
private final OnCurrentProgramUpdatedListener mOnCurrentProgramUpdatedListener =
new OnCurrentProgramUpdatedListener() {
@Override
public void onCurrentProgramUpdated(long channelId, Program program) {
// Do not update channel banner by this notification
// when the time shifting is available.
if (mTimeShiftManager.isAvailable()) {
return;
}
Channel channel = mTvView.getCurrentChannel();
if (channel != null && channel.getId() == channelId) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
updateMediaSession();
}
}
};
private final ChannelTuner.Listener mChannelTunerListener =
new ChannelTuner.Listener() {
@Override
public void onLoadFinished() {
SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable();
if (mActivityResumed) {
resumeTvIfNeeded();
resumePipIfNeeded();
}
mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList());
mHandler.post(new Runnable() {
@Override
public void run() {
mOverlayManager.getMenu().setChannelTuner(mChannelTuner);
}
});
}
@Override
public void onBrowsableChannelListChanged() {
mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList());
}
@Override
public void onCurrentChannelUnavailable(Channel channel) {
// TODO: handle the case that a channel is suddenly removed from DB.
}
@Override
public void onChannelChanged(Channel previousChannel, Channel currentChannel) {
}
};
private final Runnable mRestoreMainViewRunnable =
new Runnable() {
@Override
public void run() {
restoreMainTvView();
}
};
private ProgramGuideSearchFragment mSearchFragment;
private TvInputCallback mTvInputCallback = new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
if (mUsbTunerInputId.equals(inputId)
&& UsbTunerPreferences.shouldShowSetupActivity(MainActivity.this)) {
Intent intent = TunerSetupActivity.createSetupActivity(MainActivity.this);
startActivity(intent);
UsbTunerPreferences.setShouldShowSetupActivity(MainActivity.this, false);
SetupUtils.getInstance(MainActivity.this).markAsKnownInput(mUsbTunerInputId);
}
}
};
private void applyParentalControlSettings() {
boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings()
.isParentalControlsEnabled();
mTvView.onParentalControlChanged(parentalControlEnabled);
mPipView.onParentalControlChanged(parentalControlEnabled);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG,"onCreate()");
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
&& !PermissionUtils.hasAccessAllEpg(this)) {
Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
finish();
return;
}
boolean skipToShowOnboarding = getIntent().getAction() == Intent.ACTION_VIEW
&& TvContract.isChannelUriForPassthroughInput(getIntent().getData());
if (Features.ONBOARDING_EXPERIENCE.isEnabled(this)
&& OnboardingUtils.needToShowOnboarding(this) && !skipToShowOnboarding
&& !TvCommonUtils.isRunningInTest()) {
// TODO: The onboarding is turned off in test, because tests are broken by the
// onboarding. We need to enable the feature for tests later.
startActivity(OnboardingActivity.buildIntent(this, getIntent()));
finish();
return;
}
TvApplication tvApplication = (TvApplication) getApplication();
tvApplication.getMainActivityWrapper().onMainActivityCreated(this);
if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
mTvInputManagerHelper.addCallback(mTvInputCallback);
mUsbTunerInputId = UsbTunerTvInputService.getInputId(this);
mChannelDataManager = tvApplication.getChannelDataManager();
mProgramDataManager = tvApplication.getProgramDataManager();
mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID,
mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper);
mChannelTuner.addListener(mChannelTunerListener);
mChannelTuner.start();
mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner);
mPipInputManager.start();
mMemoryManageables.add(mProgramDataManager);
mMemoryManageables.add(ImageCache.getInstance());
mMemoryManageables.add(TvContentRatingCache.getInstance());
if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
mDvrManager = tvApplication.getDvrManager();
mDvrDataManager = tvApplication.getDvrDataManager();
}
DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
Point size = new Point();
display.getSize(size);
int screenWidth = size.x;
int screenHeight = size.y;
mDefaultRefreshRate = display.getRefreshRate();
mOverlayRootView = (OverlayRootView) getLayoutInflater().inflate(
R.layout.overlay_root_view, null, false);
setContentView(R.layout.activity_tv);
mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
int shrunkenTvViewHeight = getResources().getDimensionPixelSize(
R.dimen.shrunken_tvview_height);
mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), false, screenHeight,
shrunkenTvViewHeight);
mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
if (isKeyEventBlocked()) {
return true;
}
if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event;
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.isLongPress()) {
if (onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) {
return true;
}
}
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
return onKeyUp(keyEvent.getKeyCode(), keyEvent);
} else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
return onKeyDown(keyEvent.getKeyCode(), keyEvent);
}
}
return false;
}
});
mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, mTracker,
new OnCurrentProgramUpdatedListener() {
@Override
public void onCurrentProgramUpdated(long channelId, Program program) {
updateMediaSession();
switch (mTimeShiftManager.getLastActionId()) {
case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT:
updateChannelBannerAndShowIfNeeded(
UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
break;
default:
updateChannelBannerAndShowIfNeeded(
UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
break;
}
}
});
mPipView = (TunableTvView) findViewById(R.id.pip_tunable_tv_view);
mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight,
shrunkenTvViewHeight);
if (!PermissionUtils.hasAccessWatchedHistory(this)) {
WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager(
getApplicationContext());
watchedHistoryManager.start();
mTvView.setWatchedHistoryManager(watchedHistoryManager);
}
mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView,
(FrameLayout) findViewById(android.R.id.content), mTvOptionsManager);
mPipView.setFixedSurfaceSize(screenWidth / 2, screenHeight / 2);
mPipView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW);
ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container);
mChannelBannerView = (ChannelBannerView) getLayoutInflater().inflate(
R.layout.channel_banner, sceneContainer, false);
mKeypadChannelSwitchView = (KeypadChannelSwitchView) getLayoutInflater().inflate(
R.layout.keypad_channel_switch, sceneContainer, false);
InputBannerView inputBannerView = (InputBannerView) getLayoutInflater()
.inflate(R.layout.input_banner, sceneContainer, false);
SelectInputView selectInputView = (SelectInputView) getLayoutInflater()
.inflate(R.layout.select_input, sceneContainer, false);
selectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() {
@Override
public void onTunerInputSelected() {
Channel currentChannel = mChannelTuner.getCurrentChannel();
if (currentChannel != null && !currentChannel.isPassthrough()) {
hideOverlays();
} else {
tuneToLastWatchedChannelForTunerInput();
}
}
@Override
public void onPassthroughInputSelected(TvInputInfo input) {
Channel currentChannel = mChannelTuner.getCurrentChannel();
String currentInputId = currentChannel == null ? null : currentChannel.getInputId();
if (TextUtils.equals(input.getId(), currentInputId)) {
hideOverlays();
} else {
tuneToChannel(Channel.createPassthroughChannel(input.getId()));
}
}
private void hideOverlays() {
getOverlayManager().hideOverlays(
TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
}
});
mSearchFragment = new ProgramGuideSearchFragment();
mOverlayManager = new TvOverlayManager(this, mChannelTuner,
mKeypadChannelSwitchView, mChannelBannerView, inputBannerView,
selectInputView, sceneContainer, mSearchFragment);
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mMediaSession = new MediaSession(this, MEDIA_SESSION_TAG);
mMediaSession.setCallback(new MediaSession.Callback() {
@Override
public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
// Consume the media button event here. Should not send it to other apps.
return true;
}
});
mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mNowPlayingCardWidth = getResources().getDimensionPixelSize(
R.dimen.notif_card_img_max_width);
mNowPlayingCardHeight = getResources().getDimensionPixelSize(R.dimen.notif_card_img_height);
mTvViewUiManager.restoreDisplayMode(false);
if (!handleIntent(getIntent())) {
finish();
return;
}
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this,
new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
@Override
public void onAc3PassthroughCapabilityChange(boolean capability) {
mAc3PassthroughSupported = capability;
}
});
mAudioCapabilitiesReceiver.register();
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1),
new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper), null);
mSendConfigInfoRecurringRunner.start();
mChannelStatusRecurringRunner = SendChannelStatusRunnable
.startChannelStatusRecurringRunner(this, mTracker, mChannelDataManager);
// To avoid not updating Rating systems when changing language.
mTvInputManagerHelper.getContentRatingsManager().update();
initForTest();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
if (grantResults != null && grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Start reload of dependent data
mChannelDataManager.reload();
mProgramDataManager.reload();
// Restart live channels.
Intent intent = getIntent();
finish();
startActivity(intent);
} else {
Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
Toast.LENGTH_LONG).show();
finish();
}
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, 0, PixelFormat.TRANSPARENT);
windowParams.token = getWindow().getDecorView().getWindowToken();
((WindowManager) getSystemService(Context.WINDOW_SERVICE)).addView(mOverlayRootView,
windowParams);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
((WindowManager) getSystemService(Context.WINDOW_SERVICE)).removeView(mOverlayRootView);
}
private int getDesiredBlockScreenType() {
if (!mActivityResumed) {
return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
}
if (isUnderShrunkenTvView()) {
return TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW;
}
if (mOverlayManager.needHideTextOnMainView()) {
return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
}
SafeDismissDialogFragment currentDialog = mOverlayManager.getCurrentDialog();
if (currentDialog != null) {
// If PIN dialog is shown for unblocking the channel lock or content ratings lock,
// keeping the unlocking message is more natural instead of changing it.
if (currentDialog instanceof PinDialogFragment) {
int type = ((PinDialogFragment) currentDialog).getType();
if (type == PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL
|| type == PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) {
return TunableTvView.BLOCK_SCREEN_TYPE_NORMAL;
}
}
return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
}
if (mOverlayManager.isSetupFragmentActive()
|| mOverlayManager.isNewSourcesFragmentActive()) {
return TunableTvView.BLOCK_SCREEN_TYPE_NO_UI;
}
return TunableTvView.BLOCK_SCREEN_TYPE_NORMAL;
}
@Override
protected void onNewIntent(Intent intent) {
mOverlayManager.getSideFragmentManager().hideAll(false);
if (!handleIntent(intent) && !mActivityStarted) {
// If the activity is stopped and not destroyed, finish the activity.
// Otherwise, just ignore the intent.
finish();
}
}
@Override
protected void onStart() {
if (DEBUG) Log.d(TAG,"onStart()");
super.onStart();
mScreenOffIntentReceived = false;
mActivityStarted = true;
mTracker.sendMainStart();
mMainDurationTimer.start();
applyParentalControlSettings();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
registerReceiver(mBroadcastReceiver, intentFilter);
Intent notificationIntent = new Intent(this, NotificationService.class);
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
}
@Override
protected void onResume() {
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
if (!PermissionUtils.hasAccessAllEpg(this)
&& checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
PERMISSIONS_REQUEST_READ_TV_LISTINGS);
}
mTracker.sendScreenView(SCREEN_NAME);
SystemProperties.updateSystemProperties();
mNeedShowBackKeyGuide = true;
mActivityResumed = true;
mShowNewSourcesFragment = true;
mOtherActivityLaunched = false;
int result = mAudioManager.requestAudioFocus(MainActivity.this,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
setVolumeByAudioFocusStatus();
if (mTvView.isPlaying()) {
// Every time onResume() is called the activity will be assumed to not have requested
// visible behind.
requestVisibleBehind(true);
}
if (mChannelTuner.areAllChannelsLoaded()) {
SetupUtils.getInstance(this).markNewChannelsBrowsable();
resumeTvIfNeeded();
resumePipIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
// Note: The following codes are related to pop up an overlay UI after resume.
// When the following code is changed, please check the variable
// willShowOverlayUiAfterResume in updateChannelBannerAndShowIfNeeded.
if (mInputToSetUp != null) {
startSetupActivity(mInputToSetUp, false);
mInputToSetUp = null;
} else if (mShowProgramGuide) {
mShowProgramGuide = false;
mHandler.post(new Runnable() {
// This will delay the start of the animation until after the Live Channel app is
// shown. Without this the animation is completed before it is actually visible on
// the screen.
@Override
public void run() {
mOverlayManager.showProgramGuide();
}
});
} else if (mShowSelectInputView) {
mShowSelectInputView = false;
mHandler.post(new Runnable() {
// mShowSelectInputView is true when the activity is started/resumed because the
// TV_INPUT button was pressed in a different app.
// This will delay the start of the animation until after the Live Channel app is
// shown. Without this the animation is completed before it is actually visible on
// the screen.
@Override
public void run() {
mOverlayManager.showSelectInputView();
}
});
}
}
@Override
protected void onPause() {
if (DEBUG) Log.d(TAG, "onPause()");
finishChannelChangeIfNeeded();
mActivityResumed = false;
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT);
mTvView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_NO_UI);
if (mPipEnabled) {
mTvViewUiManager.hidePipForPause();
}
mBackKeyPressed = false;
mShowLockedChannelsTemporarily = false;
mShouldTuneToTunerChannel = false;
if (!mVisibleBehind) {
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
mMediaSession.setActive(false);
}
mTracker.sendScreenView("");
} else {
mTracker.sendScreenView(SCREEN_BEHIND_NAME);
}
super.onPause();
}
/**
* Returns true if {@link #onResume} is called and {@link #onPause} is not called yet.
*/
public boolean isActivityResumed() {
return mActivityResumed;
}
/**
* Returns true if {@link #onStart} is called and {@link #onStop} is not called yet.
*/
public boolean isActivityStarted() {
return mActivityStarted;
}
@Override
public boolean requestVisibleBehind(boolean enable) {
boolean state = super.requestVisibleBehind(enable);
mVisibleBehind = state;
return state;
}
private void resumeTvIfNeeded() {
if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()");
if (!mTvView.isPlaying() || mInitChannelUri != null
|| (mShouldTuneToTunerChannel && mChannelTuner.isCurrentChannelPassthrough())) {
if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {
// The target input may not be ready yet, especially, just after screen on.
String inputId = mInitChannelUri.getPathSegments().get(1);
TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(inputId);
if (input == null) {
input = mTvInputManagerHelper.getTvInputInfo(mParentInputIdWhenScreenOff);
if (input == null) {
SoftPreconditions.checkState(false, TAG, "Input disappear." + input);
finish();
} else {
mInitChannelUri =
TvContract.buildChannelUriForPassthroughInput(input.getId());
}
}
}
mParentInputIdWhenScreenOff = null;
startTv(mInitChannelUri);
mInitChannelUri = null;
}
// Make sure TV app has the main TV view to handle the case that TvView is used in other
// application.
restoreMainTvView();
mTvView.setBlockScreenType(getDesiredBlockScreenType());
}
private void resumePipIfNeeded() {
if (mPipEnabled && !(mPipView.isPlaying() && mPipView.isShown())) {
if (mPipInputManager.areInSamePipInput(
mChannelTuner.getCurrentChannel(), mPipChannel)) {
enablePipView(false, false);
} else {
if (!mPipView.isPlaying()) {
startPip(false);
} else {
mTvViewUiManager.showPipForResume();
}
}
}
}
private void startTv(Uri channelUri) {
if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri);
if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))
&& mChannelTuner.isCurrentChannelPassthrough()) {
// For passthrough TV input, channelUri is always given. If TV app is launched
// by TV app icon in a launcher, channelUri is null. So if passthrough TV input
// is playing, we stop the passthrough TV input.
stopTv();
}
SoftPreconditions.checkState(TvContract.isChannelUriForPassthroughInput(channelUri)
|| mChannelTuner.areAllChannelsLoaded(),
TAG, "startTV assumes that ChannelDataManager is already loaded.");
if (mTvView.isPlaying()) {
// TV has already started.
if (channelUri == null) {
// Simply adjust the volume without tune.
setVolumeByAudioFocusStatus();
return;
}
if (channelUri.equals(mChannelTuner.getCurrentChannelUri())) {
// The requested channel is already tuned.
setVolumeByAudioFocusStatus();
return;
}
stopTv();
}
if (mChannelTuner.getCurrentChannel() != null) {
Log.w(TAG, "The current channel should be reset before");
mChannelTuner.resetCurrentChannel();
}
if (channelUri == null) {
// If any initial channel id is not given, remember the last channel the user watched.
long channelId = Utils.getLastWatchedChannelId(this);
if (channelId != Channel.INVALID_ID) {
channelUri = TvContract.buildChannelUri(channelId);
}
}
if (channelUri == null) {
mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0));
} else {
if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
Channel channel = Channel.createPassthroughChannel(channelUri);
mChannelTuner.moveToChannel(channel);
} else {
long channelId = ContentUris.parseId(channelUri);
Channel channel = mChannelDataManager.getChannel(channelId);
if (channel == null || !mChannelTuner.moveToChannel(channel)) {
mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0));
Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. "
+ "The first channel will be tuned to.");
}
}
}
mTvView.start(mTvInputManagerHelper);
setVolumeByAudioFocusStatus();
if (mRecordingUri != null) {
playRecording(mRecordingUri);
mRecordingUri = null;
} else {
tune();
}
}
@Override
protected void onStop() {
if (DEBUG) Log.d(TAG, "onStop()");
if (mScreenOffIntentReceived) {
mScreenOffIntentReceived = false;
} else {
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (!powerManager.isInteractive()) {
// We added to check isInteractive as well as SCREEN_OFF intent, because
// calling timing of the intent SCREEN_OFF is not consistent. b/25953633.
// If we verify that checking isInteractive is enough, we can remove the logic
// for SCREEN_OFF intent.
markCurrentChannelDuringScreenOff();
}
}
mActivityStarted = false;
stopAll(false);
unregisterReceiver(mBroadcastReceiver);
mTracker.sendMainStop(mMainDurationTimer.reset());
super.onStop();
}
/**
* Handles screen off to keep the current channel for next screen on.
*/
private void markCurrentChannelDuringScreenOff() {
mInitChannelUri = mChannelTuner.getCurrentChannelUri();
if (mChannelTuner.isCurrentChannelPassthrough()) {
// When ACTION_SCREEN_OFF is invoked, some CEC devices may be already
// removed. So we need to get the input info from ChannelTuner instead of
// TvInputManagerHelper.
TvInputInfo input = mChannelTuner.getCurrentInputInfo();
mParentInputIdWhenScreenOff = input.getParentId();
if (DEBUG) Log.d(TAG, "Parent input: " + mParentInputIdWhenScreenOff);
}
}
private void stopAll(boolean keepVisibleBehind) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
stopTv("stopAll()", keepVisibleBehind);
stopPip();
}
public TvInputManagerHelper getTvInputManagerHelper() {
return mTvInputManagerHelper;
}
/**
* Starts setup activity for the given input {@code input}.
*
* @param calledByPopup If true, startSetupActivity is invoked from the setup fragment.
*/
public void startSetupActivity(TvInputInfo input, boolean calledByPopup) {
Intent intent = TvCommonUtils.createSetupIntent(input);
if (intent == null) {
Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show();
return;
}
// Even though other app can handle the intent, the setup launched by Live channels
// should go through Live channels SetupPassthroughActivity.
intent.setComponent(new ComponentName(this, SetupPassthroughActivity.class));
try {
// Now we know that the user intends to set up this input. Grant permission for writing
// EPG data.
SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName);
mInputIdUnderSetup = input.getId();
mIsSetupActivityCalledByPopup = calledByPopup;
// Call requestVisibleBehind(false) before starting other activity.
// In Activity.requestVisibleBehind(false), this activity is scheduled to be stopped
// immediately if other activity is about to start. And this activity is scheduled to
// to be stopped again after onPause().
stopTv("startSetupActivity()", false);
startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
} catch (ActivityNotFoundException e) {
mInputIdUnderSetup = null;
Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity,
input.loadLabel(this)), Toast.LENGTH_SHORT).show();
return;
}
if (calledByPopup) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
} else {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY);
}
}
public boolean hasCaptioningSettingsActivity() {
return Utils.isIntentAvailable(this, new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
}
public void startSystemCaptioningSettingsActivity() {
Intent intent = new Intent(Settings.ACTION_CAPTIONING_SETTINGS);
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY);
try {
startActivityForResultSafe(intent, REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, getString(R.string.msg_unable_to_start_system_captioning_settings),
Toast.LENGTH_SHORT).show();
}
}
public ChannelDataManager getChannelDataManager() {
return mChannelDataManager;
}
public ProgramDataManager getProgramDataManager() {
return mProgramDataManager;
}
public PipInputManager getPipInputManager() {
return mPipInputManager;
}
public TvOptionsManager getTvOptionsManager() {
return mTvOptionsManager;
}
public TvViewUiManager getTvViewUiManager() {
return mTvViewUiManager;
}
public TimeShiftManager getTimeShiftManager() {
return mTimeShiftManager;
}
/**
* Returns the instance of {@link TvOverlayManager}.
*/
public TvOverlayManager getOverlayManager() {
return mOverlayManager;
}
public Channel getCurrentChannel() {
return mTvView.isRecordingPlayback() ? mTvView.getCurrentChannel()
: mChannelTuner.getCurrentChannel();
}
public long getCurrentChannelId() {
if (mTvView.isRecordingPlayback()) {
Channel channel = mTvView.getCurrentChannel();
return channel == null ? Channel.INVALID_ID : channel.getId();
}
return mChannelTuner.getCurrentChannelId();
}
/**
* Returns true if the current connected TV supports AC3 passthough.
*/
public boolean isAc3PassthroughSupported() {
return mAc3PassthroughSupported;
}
/**
* Returns the current program which the user is watching right now.<p>
*
* If the time shifting is available, it can be a past program.
*/
public Program getCurrentProgram() {
return getCurrentProgram(true);
}
/**
* Returns {@code true}, if this view is the recording playback mode.
*/
public boolean isRecordingPlayback() {
return mTvView.isRecordingPlayback();
}
/**
* Returns the recording which is being played right now.
*/
public RecordedProgram getPlayingRecordedProgram() {
return mTvView.getPlayingRecordedProgram();
}
/**
* Returns the current program which the user is watching right now.<p>
*
* @param applyTimeShifted If it is true and the time shifting is available, it can be
* a past program.
*/
public Program getCurrentProgram(boolean applyTimeShifted) {
if (applyTimeShifted && mTimeShiftManager.isAvailable()) {
return mTimeShiftManager.getCurrentProgram();
}
return mProgramDataManager.getCurrentProgram(getCurrentChannelId());
}
/**
* Returns the current playing time in milliseconds.<p>
*
* If the time shifting is available, the time is the playing position of the program,
* otherwise, the system current time.
*/
public long getCurrentPlayingPosition() {
if (mTimeShiftManager.isAvailable()) {
return mTimeShiftManager.getCurrentPositionMs();
}
return System.currentTimeMillis();
}
public Channel getBrowsableChannel() {
// TODO: mChannelMap could be dirty for a while when the browsablity of channels
// are changed. In that case, we shouldn't use the value from mChannelMap.
Channel curChannel = mChannelTuner.getCurrentChannel();
if (curChannel != null && curChannel.isBrowsable()) {
return curChannel;
} else {
return mChannelTuner.getAdjacentBrowsableChannel(true);
}
}
/**
* Call {@link Activity#startActivity} in a safe way.
*
* @see LauncherActivity
*/
public void startActivitySafe(Intent intent) {
LauncherActivity.startActivitySafe(this, intent);
}
/**
* Call {@link Activity#startActivityForResult} in a safe way.
*
* @see LauncherActivity
*/
private void startActivityForResultSafe(Intent intent, int requestCode) {
LauncherActivity.startActivityForResultSafe(this, intent, requestCode);
}
/**
* Show settings fragment.
*/
public void showSettingsFragment() {
if (!mChannelTuner.areAllChannelsLoaded()) {
// Show ChannelSourcesFragment only if all the channels are loaded.
return;
}
Channel currentChannel = mChannelTuner.getCurrentChannel();
long channelId = currentChannel == null ? Channel.INVALID_ID : currentChannel.getId();
mOverlayManager.getSideFragmentManager().show(new SettingsFragment(channelId));
}
public void showMerchantCollection() {
startActivitySafe(OnboardingUtils.PLAY_STORE_INTENT);
}
/**
* It is called when shrunken TvView is desired, such as EditChannelFragment and
* ChannelsLockedFragment.
*/
public void startShrunkenTvView(boolean showLockedChannelsTemporarily,
boolean willMainViewBeTunerInput) {
mChannelBeforeShrunkenTvView = mTvView.getCurrentChannel();
mWasChannelUnblockedBeforeShrunkenByUser = mIsCurrentChannelUnblockedByUser;
mAllowedRatingBeforeShrunken = mLastAllowedRatingForCurrentChannel;
if (willMainViewBeTunerInput && mChannelTuner.isCurrentChannelPassthrough()
&& mPipEnabled) {
mPipChannelBeforeShrunkenTvView = mPipChannel;
enablePipView(false, false);
} else {
mPipChannelBeforeShrunkenTvView = null;
}
mTvViewUiManager.startShrunkenTvView();
if (showLockedChannelsTemporarily) {
mShowLockedChannelsTemporarily = true;
checkChannelLockNeeded(mTvView);
}
mTvView.setBlockScreenType(getDesiredBlockScreenType());
}
/**
* It is called when shrunken TvView is no longer desired, such as EditChannelFragment and
* ChannelsLockedFragment.
*/
public void endShrunkenTvView() {
mTvViewUiManager.endShrunkenTvView();
mIsCompletingShrunkenTvView = true;
Channel returnChannel = mChannelBeforeShrunkenTvView;
if (returnChannel == null
|| (!returnChannel.isPassthrough() && !returnChannel.isBrowsable())) {
// Try to tune to the next best channel instead.
returnChannel = getBrowsableChannel();
}
mShowLockedChannelsTemporarily = false;
// The current channel is mTvView.getCurrentChannel() and need to tune to the returnChannel.
if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) {
final Channel channel = returnChannel;
Runnable tuneAction = new Runnable() {
@Override
public void run() {
tuneToChannel(channel);
if (mChannelBeforeShrunkenTvView == null
|| !mChannelBeforeShrunkenTvView.equals(channel)) {
Utils.setLastWatchedChannel(MainActivity.this, channel);
}
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
if (mPipChannelBeforeShrunkenTvView != null) {
enablePipView(true, false);
mPipChannelBeforeShrunkenTvView = null;
}
}
};
mTvViewUiManager.fadeOutTvView(tuneAction);
// Will automatically fade-in when video becomes available.
} else {
checkChannelLockNeeded(mTvView);
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
if (mPipChannelBeforeShrunkenTvView != null) {
enablePipView(true, false);
mPipChannelBeforeShrunkenTvView = null;
}
}
}
private boolean isUnderShrunkenTvView() {
return mTvViewUiManager.isUnderShrunkenTvView() || mIsCompletingShrunkenTvView;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_START_SETUP_ACTIVITY:
if (resultCode == RESULT_OK) {
int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup);
String text;
if (count > 0) {
text = getResources().getQuantityString(R.plurals.msg_channel_added,
count, count);
} else {
text = getString(R.string.msg_no_channel_added);
}
Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
mInputIdUnderSetup = null;
if (mChannelTuner.getCurrentChannel() == null) {
mChannelTuner.moveToAdjacentBrowsableChannel(true);
}
if (mTunePending) {
tune();
}
} else {
mInputIdUnderSetup = null;
}
if (!mIsSetupActivityCalledByPopup) {
mOverlayManager.getSideFragmentManager().showSidePanel(false);
}
break;
case REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS:
mOverlayManager.getSideFragmentManager().showSidePanel(false);
break;
}
if (data != null) {
String errorMessage = data.getStringExtra(LauncherActivity.ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public View findViewById(int id) {
// In order to locate fragments in non-application window, we should override findViewById.
// Internally, Activity.findViewById is called to attach a view of a fragment into its
// container. Without the override, we'll get crash during the fragment attachment.
View v = mOverlayRootView != null ? mOverlayRootView.findViewById(id) : null;
return v == null ? super.findViewById(id) : v;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
// If an activity is closed on a back key down event, back key down events with none zero
// repeat count or a back key up event can be happened without the first back key down
// event which should be ignored in this activity.
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
mBackKeyPressed = true;
}
if (!mBackKeyPressed) {
return true;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
mBackKeyPressed = false;
}
}
// When side panel is closing, it has the focus.
// Keep the focus, but just don't deliver the key events.
if ((mOverlayRootView.hasFocusable()
&& !mOverlayManager.getSideFragmentManager().isHiding())
|| mOverlayManager.getSideFragmentManager().isActive()) {
return super.dispatchKeyEvent(event);
}
if (BLACKLIST_KEYCODE_TO_TIS.contains(event.getKeyCode())
|| KeyEvent.isGamepadButton(event.getKeyCode())) {
// If the event is in blacklisted or gamepad key, do not pass it to session.
// Gamepad keys are blacklisted to support TV UIs and here's the detail.
// If there's a TIS granted RECEIVE_INPUT_EVENT, TIF sends key events to TIS
// and return immediately saying that the event is handled.
// In this case, fallback key will be injected but with FLAG_CANCELED
// while gamepads support DPAD_CENTER and BACK by fallback.
// Since we don't expect that TIS want to handle gamepad buttons now,
// blacklist gamepad buttons and wait for next fallback keys.
// TODO) Need to consider other fallback keys (e.g. ESCAPE)
return super.dispatchKeyEvent(event);
}
return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event);
}
@Override
public void onAudioFocusChange(int focusChange) {
mAudioFocusStatus = focusChange;
setVolumeByAudioFocusStatus();
}
/**
* Notifies the key input focus is changed to the TV view.
*/
public void updateKeyInputFocus() {
mHandler.post(new Runnable() {
@Override
public void run() {
mTvView.setBlockScreenType(getDesiredBlockScreenType());
}
});
}
// It should be called before onResume.
private boolean handleIntent(Intent intent) {
// Reset the closed caption settings when the activity is 1)created or 2) restarted.
// And do not reset while TvView is playing.
if (!mTvView.isPlaying()) {
mCaptionSettings = new CaptionSettings(this);
}
// Handle the passed key press, if any. Note that only the key codes that are currently
// handled in the TV app will be handled via Intent.
// TODO: Consider defining a separate intent filter as passing data of mime type
// vnd.android.cursor.item/channel isn't really necessary here.
int keyCode = intent.getIntExtra(Utils.EXTRA_KEY_KEYCODE, KeyEvent.KEYCODE_UNKNOWN);
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode);
KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
onKeyUp(keyCode, event);
return true;
}
mShouldTuneToTunerChannel = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false);
mInitChannelUri = null;
String extraAction = intent.getStringExtra(Utils.EXTRA_KEY_ACTION);
if (!TextUtils.isEmpty(extraAction)) {
if (DEBUG) Log.d(TAG, "Got an extra action: " + extraAction);
if (Utils.EXTRA_ACTION_SHOW_TV_INPUT.equals(extraAction)) {
String lastWatchedChannelUri = Utils.getLastWatchedChannelUri(this);
if (lastWatchedChannelUri != null) {
mInitChannelUri = Uri.parse(lastWatchedChannelUri);
}
mShowSelectInputView = true;
}
}
if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
mRecordingUri = intent.getParcelableExtra(Utils.EXTRA_KEY_RECORDING_URI);
if (mRecordingUri != null) {
return true;
}
}
// TODO: remove the checkState once N API is finalized.
SoftPreconditions.checkState(TvInputManager.ACTION_SETUP_INPUTS.equals(
"android.media.tv.action.SETUP_INPUTS"));
if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
runAfterAttachedToWindow(new Runnable() {
@Override
public void run() {
mOverlayManager.showSetupFragment();
}
});
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
try {
mSource = uri.getQueryParameter(Utils.PARAM_SOURCE);
} catch (UnsupportedOperationException e) {
// ignore this exception.
}
// When the URI points to the programs (directory, not an individual item), go to the
// program guide. The intention here is to respond to
// "content://android.media.tv/program", not "content://android.media.tv/program/XXX".
// Later, we might want to add handling of individual programs too.
if (Utils.isProgramsUri(uri)) {
// The given data is a programs URI. Open the Program Guide.
mShowProgramGuide = true;
return true;
}
// In case the channel is given explicitly, use it.
mInitChannelUri = uri;
if (DEBUG) Log.d(TAG, "ACTION_VIEW with " + mInitChannelUri);
if (Channels.CONTENT_URI.equals(mInitChannelUri)) {
// Tune to default channel.
mInitChannelUri = null;
mShouldTuneToTunerChannel = true;
return true;
}
if ((!Utils.isChannelUriForOneChannel(mInitChannelUri)
&& !Utils.isChannelUriForInput(mInitChannelUri))) {
Log.w(TAG, "Malformed channel uri " + mInitChannelUri
+ " tuning to default instead");
mInitChannelUri = null;
return true;
}
mTuneParams = intent.getExtras();
if (mTuneParams == null) {
mTuneParams = new Bundle();
}
if (Utils.isChannelUriForTunerInput(mInitChannelUri)) {
long channelId = ContentUris.parseId(mInitChannelUri);
mTuneParams.putLong(KEY_INIT_CHANNEL_ID, channelId);
} else if (TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {
// If mInitChannelUri is for a passthrough TV input.
String inputId = mInitChannelUri.getPathSegments().get(1);
TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(inputId);
if (input == null) {
mInitChannelUri = null;
Toast.makeText(this, R.string.msg_no_specific_input, Toast.LENGTH_SHORT).show();
return false;
} else if (!input.isPassthroughInput()) {
mInitChannelUri = null;
Toast.makeText(this, R.string.msg_not_passthrough_input, Toast.LENGTH_SHORT)
.show();
return false;
}
} else if (mInitChannelUri != null) {
// Handle the URI built by TvContract.buildChannelsUriForInput().
// TODO: Change hard-coded "input" to TvContract.PARAM_INPUT.
String inputId = mInitChannelUri.getQueryParameter("input");
long channelId = Utils.getLastWatchedChannelIdForInput(this, inputId);
if (channelId == Channel.INVALID_ID) {
String[] projection = { Channels._ID };
try (Cursor cursor = getContentResolver().query(uri, projection,
null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
channelId = cursor.getLong(0);
}
}
}
if (channelId == Channel.INVALID_ID) {
// Couldn't find any channel probably because the input hasn't been set up.
// Try to set it up.
mInitChannelUri = null;
mInputToSetUp = mTvInputManagerHelper.getTvInputInfo(inputId);
} else {
mInitChannelUri = TvContract.buildChannelUri(channelId);
mTuneParams.putLong(KEY_INIT_CHANNEL_ID, channelId);
}
}
}
return true;
}
private void setVolumeByAudioFocusStatus() {
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
setVolumeByAudioFocusStatus(mTvView);
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
setVolumeByAudioFocusStatus(mPipView);
}
}
private void setVolumeByAudioFocusStatus(TunableTvView tvView) {
SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView);
if (tvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
tvView.setStreamVolume(AUDIO_MAX_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
tvView.setStreamVolume(AUDIO_MIN_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
tvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
break;
}
}
if (tvView == mTvView) {
if (mPipView != null && mPipView.isPlaying()) {
mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
}
} else { // tvView == mPipView
if (mTvView != null && mTvView.isPlaying()) {
mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
}
}
}
private void stopTv() {
stopTv(null, false);
}
private void stopTv(String logForCaller, boolean keepVisibleBehind) {
if (logForCaller != null) {
Log.i(TAG, "stopTv is called at " + logForCaller + ".");
} else {
if (DEBUG) Log.d(TAG, "stopTv()");
}
if (mTvView.isPlaying()) {
mTvView.stop();
if (!keepVisibleBehind) {
requestVisibleBehind(false);
}
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
mMediaSession.setActive(false);
}
}
TvApplication.getSingletons(this).getMainActivityWrapper()
.notifyCurrentChannelChange(this, null);
mChannelTuner.resetCurrentChannel();
mTunePending = false;
}
private boolean isPlaying() {
return mTvView.isPlaying() && mTvView.getCurrentChannel() != null;
}
private void startPip(final boolean fromUserInteraction) {
if (mPipChannel == null) {
Log.w(TAG, "PIP channel id is an invalid id.");
return;
}
if (DEBUG) Log.d(TAG, "startPip() " + mPipChannel);
mPipView.start(mTvInputManagerHelper);
boolean success = mPipView.tuneTo(mPipChannel, null, new OnTuneListener() {
@Override
public void onUnexpectedStop(Channel channel) {
Log.w(TAG, "The PIP is Unexpectedly stopped");
enablePipView(false, false);
}
@Override
public void onTuneFailed(Channel channel) {
Log.w(TAG, "Fail to start the PIP during channel tuning");
if (fromUserInteraction) {
Toast.makeText(MainActivity.this, R.string.msg_no_pip_support,
Toast.LENGTH_SHORT).show();
enablePipView(false, false);
}
}
@Override
public void onStreamInfoChanged(StreamInfo info) {
mTvViewUiManager.updatePipView();
mHandler.removeCallbacks(mRestoreMainViewRunnable);
restoreMainTvView();
}
@Override
public void onChannelRetuned(Uri channel) {
if (channel == null) {
return;
}
Channel currentChannel =
mChannelDataManager.getChannel(ContentUris.parseId(channel));
if (currentChannel == null) {
Log.e(TAG, "onChannelRetuned is called from PIP input but can't find a channel"
+ " with the URI " + channel);
return;
}
if (isChannelChangeKeyDownReceived()) {
// Ignore this message if the user is changing the channel.
return;
}
mPipChannel = currentChannel;
mPipView.setCurrentChannel(mPipChannel);
}
@Override
public void onContentBlocked() {
updateMediaSession();
}
@Override
public void onContentAllowed() {
updateMediaSession();
}
});
if (!success) {
Log.w(TAG, "Fail to start the PIP");
return;
}
if (fromUserInteraction) {
checkChannelLockNeeded(mPipView);
}
// Explicitly make the PIP view main to make the selected input an HDMI-CEC active source.
mPipView.setMain();
scheduleRestoreMainTvView();
mTvViewUiManager.onPipStart();
setVolumeByAudioFocusStatus();
}
private void scheduleRestoreMainTvView() {
mHandler.removeCallbacks(mRestoreMainViewRunnable);
mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS);
}
private void stopPip() {
if (DEBUG) Log.d(TAG, "stopPip");
if (mPipView.isPlaying()) {
mPipView.stop();
mPipSwap = false;
mTvViewUiManager.onPipStop();
}
}
/**
* Says {@code text} when accessibility is turned on.
*/
public void sendAccessibilityText(String text) {
if (mAccessibilityManager.isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setClassName(getClass().getName());
event.setPackageName(getPackageName());
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(text);
mAccessibilityManager.sendAccessibilityEvent(event);
}
}
private void playRecording(Uri recordingUri) {
mTvView.playRecording(recordingUri, mOnTuneListener);
mOnTuneListener.onPlayRecording();
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
private void tune() {
if (DEBUG) Log.d(TAG, "tune()");
mTuneDurationTimer.start();
lazyInitializeIfNeeded(LAZY_INITIALIZATION_DELAY);
// Prerequisites to be able to tune.
if (mInputIdUnderSetup != null) {
mTunePending = true;
return;
}
mTunePending = false;
final Channel channel = mChannelTuner.getCurrentChannel();
if (!mChannelTuner.isCurrentChannelPassthrough()) {
if (mTvInputManagerHelper.getTunerTvInputSize() == 0) {
Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show();
// TODO: Direct the user to a Play Store landing page for TvInputService apps.
finish();
return;
}
SetupUtils setupUtils = SetupUtils.getInstance(this);
if (setupUtils.isFirstTune()) {
if (!mChannelTuner.areAllChannelsLoaded()) {
// tune() will be called, once all channels are loaded.
stopTv("tune()", false);
return;
}
if (mChannelDataManager.getChannelCount() > 0) {
mOverlayManager.showIntroDialog();
} else if (!Features.ONBOARDING_EXPERIENCE.isEnabled(this)) {
mOverlayManager.showSetupFragment();
return;
}
}
if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
&& setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
// Show new channel sources fragment.
runAfterAttachedToWindow(new Runnable() {
@Override
public void run() {
mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
@Override
public void run() {
mOverlayManager.showNewSourcesFragment();
}
});
}
});
}
mShowNewSourcesFragment = false;
if (mChannelTuner.getBrowsableChannelCount() == 0
&& mChannelDataManager.getChannelCount() > 0
&& !mOverlayManager.getSideFragmentManager().isActive()) {
if (!mChannelTuner.areAllChannelsLoaded()) {
return;
}
if (mTvInputManagerHelper.getTunerTvInputSize() == 1) {
mOverlayManager.getSideFragmentManager().show(
new CustomizeChannelListFragment());
} else {
showSettingsFragment();
}
return;
}
// TODO: need to refactor the following code to put in startTv.
if (channel == null) {
// There is no channel to tune to.
stopTv("tune()", false);
if (!mChannelDataManager.isDbLoadFinished()) {
// Wait until channel data is loaded in order to know the number of channels.
// tune() will be retried, once the channel data is loaded.
return;
}
if (mOverlayManager.getSideFragmentManager().isActive()) {
return;
}
mOverlayManager.showSetupFragment();
return;
}
setupUtils.onTuned();
if (mTuneParams != null) {
Long initChannelId = mTuneParams.getLong(KEY_INIT_CHANNEL_ID);
if (initChannelId == channel.getId()) {
mTuneParams.remove(KEY_INIT_CHANNEL_ID);
} else {
mTuneParams = null;
}
}
}
mIsCurrentChannelUnblockedByUser = false;
if (!isUnderShrunkenTvView()) {
mLastAllowedRatingForCurrentChannel = null;
}
mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE);
if (mAccessibilityManager.isEnabled()) {
// For every tune, we need to inform the tuned channel or input to a user,
// if Talkback is turned on.
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setClassName(getClass().getName());
event.setPackageName(getPackageName());
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
if (TvContract.isChannelUriForPassthroughInput(channel.getUri())) {
TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(channel.getInputId());
event.getText().add(Utils.loadLabel(this, input));
} else if (TextUtils.isEmpty(channel.getDisplayName())) {
event.getText().add(channel.getDisplayNumber());
} else {
event.getText().add(channel.getDisplayNumber() + " " + channel.getDisplayName());
}
mAccessibilityManager.sendAccessibilityEvent(event);
}
boolean success = mTvView.tuneTo(channel, mTuneParams, mOnTuneListener);
mOnTuneListener.onTune(channel, isUnderShrunkenTvView());
mTuneParams = null;
if (!success) {
Toast.makeText(this, R.string.msg_tune_failed, Toast.LENGTH_SHORT).show();
return;
}
// Explicitly make the TV view main to make the selected input an HDMI-CEC active source.
mTvView.setMain();
scheduleRestoreMainTvView();
if (!isUnderShrunkenTvView()) {
if (!channel.isPassthrough()) {
addToRecentChannels(channel.getId());
}
Utils.setLastWatchedChannel(this, channel);
TvApplication.getSingletons(this).getMainActivityWrapper()
.notifyCurrentChannelChange(this, channel);
}
checkChannelLockNeeded(mTvView);
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
if (mActivityResumed) {
// requestVisibleBehind should be called after onResume() is called. But, when
// launcher is over the TV app and the screen is turned off and on, tune() can
// be called during the pause state by mBroadcastReceiver (Intent.ACTION_SCREEN_ON).
requestVisibleBehind(true);
}
updateMediaSession();
}
private void runAfterAttachedToWindow(final Runnable runnable) {
if (mOverlayRootView.isLaidOut()) {
runnable.run();
} else {
mOverlayRootView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
mOverlayRootView.removeOnAttachStateChangeListener(this);
runnable.run();
}
@Override
public void onViewDetachedFromWindow(View v) { }
});
}
}
private void updateMediaSession() {
if (getCurrentChannel() == null) {
mMediaSession.setActive(false);
return;
}
// If the channel is blocked, display a lock and a short text on the Now Playing Card
if (mTvView.isScreenBlocked() || mTvView.getBlockedContentRating() != null) {
setMediaSessionPlaybackState(false);
Bitmap art = BitmapFactory.decodeResource(
getResources(), R.drawable.ic_message_lock_preview);
updateMediaMetadata(
getResources().getString(R.string.channel_banner_locked_channel_title), art);
mMediaSession.setActive(true);
return;
}
final Program program = getCurrentProgram();
String cardTitleText = program == null ? null : program.getTitle();
if (TextUtils.isEmpty(cardTitleText)) {
cardTitleText = getCurrentChannel().getDisplayName();
}
updateMediaMetadata(cardTitleText, null);
setMediaSessionPlaybackState(true);
if (program != null && program.getPosterArtUri() != null) {
program.loadPosterArt(MainActivity.this, mNowPlayingCardWidth, mNowPlayingCardHeight,
createProgramPosterArtCallback(MainActivity.this, program));
} else {
updateMediaMetadataWithAlternativeArt(program);
}
mMediaSession.setActive(true);
}
private static ImageLoader.ImageLoaderCallback<MainActivity> createProgramPosterArtCallback(
MainActivity mainActivity, final Program program) {
return new ImageLoader.ImageLoaderCallback<MainActivity>(mainActivity) {
@Override
public void onBitmapLoaded(MainActivity mainActivity, @Nullable Bitmap posterArt) {
if (program != mainActivity.getCurrentProgram()
|| mainActivity.getCurrentChannel() == null) {
return;
}
mainActivity.updateProgramPosterArt(program, posterArt);
}
};
}
private void updateProgramPosterArt(Program program, @Nullable Bitmap posterArt) {
if (getCurrentChannel() == null) {
return;
}
if (posterArt != null) {
String cardTitleText = program == null ? null : program.getTitle();
if (TextUtils.isEmpty(cardTitleText)) {
cardTitleText = getCurrentChannel().getDisplayName();
}
updateMediaMetadata(cardTitleText, posterArt);
} else {
updateMediaMetadataWithAlternativeArt(program);
}
}
private void updateMediaMetadata(String title, Bitmap posterArt) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();
builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
if (posterArt != null) {
builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
}
mMediaSession.setMetadata(builder.build());
}
private void updateMediaMetadataWithAlternativeArt(final Program program) {
Channel channel = getCurrentChannel();
if (channel == null || program != getCurrentProgram()) {
return;
}
String cardTitleText;
if (channel.isPassthrough()) {
TvInputInfo input = getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
cardTitleText = Utils.loadLabel(this, input);
} else {
cardTitleText = program == null ? null : program.getTitle();
if (TextUtils.isEmpty(cardTitleText)) {
cardTitleText = channel.getDisplayName();
}
}
Bitmap posterArt = BitmapFactory.decodeResource(
getResources(), R.drawable.default_now_card);
updateMediaMetadata(cardTitleText, posterArt);
}
private void setMediaSessionPlaybackState(boolean isPlaying) {
PlaybackState.Builder builder = new PlaybackState.Builder();
builder.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_STOPPED,
PlaybackState.PLAYBACK_POSITION_UNKNOWN,
isPlaying ? MEDIA_SESSION_PLAYING_SPEED : MEDIA_SESSION_STOPPED_SPEED);
mMediaSession.setPlaybackState(builder.build());
}
private void addToRecentChannels(long channelId) {
if (!mRecentChannels.remove(channelId)) {
if (mRecentChannels.size() >= MAX_RECENT_CHANNELS) {
mRecentChannels.removeLast();
}
}
mRecentChannels.addFirst(channelId);
mOverlayManager.getMenu().onRecentChannelsChanged();
}
/**
* Returns the recently tuned channels.
*/
public ArrayDeque<Long> getRecentChannels() {
return mRecentChannels;
}
private void checkChannelLockNeeded(TunableTvView tvView) {
Channel channel = tvView.getCurrentChannel();
if (tvView.isPlaying() && channel != null) {
if (getParentalControlSettings().isParentalControlsEnabled()
&& channel.isLocked()
&& !mShowLockedChannelsTemporarily
&& !(isUnderShrunkenTvView()
&& channel.equals(mChannelBeforeShrunkenTvView)
&& mWasChannelUnblockedBeforeShrunkenByUser)) {
if (DEBUG) Log.d(TAG, "Channel " + channel.getId() + " is locked");
blockScreen(tvView);
} else {
unblockScreen(tvView);
}
}
}
private void blockScreen(TunableTvView tvView) {
tvView.blockScreen();
if (tvView == mTvView) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
updateMediaSession();
}
}
private void unblockScreen(TunableTvView tvView) {
tvView.unblockScreen();
if (tvView == mTvView) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
updateMediaSession();
}
}
/**
* Shows the channel banner if it was hidden from the side fragment.
*
* <p>When the side fragment is visible, showing the channel banner should be put off until the
* side fragment is closed even though the channel changes.
*/
public void showChannelBannerIfHiddenBySideFragment() {
if (mChannelBannerHiddenBySideFragment) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
}
}
private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) {
if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")");
if (!mChannelTuner.isCurrentChannelPassthrough() || mTvView.isRecordingPlayback()) {
int lockType = ChannelBannerView.LOCK_NONE;
if (mTvView.isScreenBlocked()) {
lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
} else if (mTvView.getBlockedContentRating() != null
|| (getParentalControlSettings().isParentalControlsEnabled()
&& !mTvView.isVideoAvailable())) {
// If the parental control is enabled, do not show the program detail until the
// video becomes available.
lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
}
if (lockType == ChannelBannerView.LOCK_NONE) {
if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) {
// Do not show detailed program information while fast-tuning.
lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
} else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
&& getParentalControlSettings().isParentalControlsEnabled()) {
// If parental control is turned on,
// assumes that program is locked by default and waits for onContentAllowed.
lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
}
}
// If lock type is not changed, we don't need to update channel banner by parental
// control.
if (!mChannelBannerView.setLockType(lockType)
&& reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) {
return;
}
mChannelBannerView.updateViews(mTvView);
}
boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW
|| reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
|| reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST);
boolean noOverlayUiWhenResume =
mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView;
if (needToShowBanner && noOverlayUiWhenResume
&& mOverlayManager.getCurrentDialog() == null
&& !mOverlayManager.isSetupFragmentActive()
&& !mOverlayManager.isNewSourcesFragmentActive()) {
if (mChannelTuner.getCurrentChannel() == null) {
mChannelBannerHiddenBySideFragment = false;
} else if (mOverlayManager.getSideFragmentManager().isActive()) {
mChannelBannerHiddenBySideFragment = true;
} else {
mChannelBannerHiddenBySideFragment = false;
mOverlayManager.showBanner();
}
}
updateAvailabilityToast();
}
/**
* Hide the overlays when tuning to a channel from the menu (e.g. Channels).
*/
public void hideOverlaysForTune() {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE);
}
public boolean needToKeepSetupScreenWhenHidingOverlay() {
return mInputIdUnderSetup != null && mIsSetupActivityCalledByPopup;
}
// For now, this only takes care of 24fps.
private void applyDisplayRefreshRate(float videoFrameRate) {
boolean is24Fps = Math.abs(videoFrameRate - FRAME_RATE_FOR_FILM) < FRAME_RATE_EPSILON;
if (mIsFilmModeSet && !is24Fps) {
setPreferredRefreshRate(mDefaultRefreshRate);
mIsFilmModeSet = false;
} else if (!mIsFilmModeSet && is24Fps) {
DisplayManager displayManager = (DisplayManager) getSystemService(
Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
float[] refreshRates = display.getSupportedRefreshRates();
for (float refreshRate : refreshRates) {
// Be conservative and set only when the display refresh rate supports 24fps.
if (Math.abs(videoFrameRate - refreshRate) < REFRESH_RATE_EPSILON) {
setPreferredRefreshRate(refreshRate);
mIsFilmModeSet = true;
return;
}
}
}
}
private void setPreferredRefreshRate(float refreshRate) {
Window window = getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.preferredRefreshRate = refreshRate;
window.setAttributes(layoutParams);
}
private void applyMultiAudio() {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks == null) {
mTvOptionsManager.onMultiAudioChanged(null);
return;
}
String id = TvSettings.getMultiAudioId(this);
String language = TvSettings.getMultiAudioLanguage(this);
int channelCount = TvSettings.getMultiAudioChannelCount(this);
TvTrackInfo bestTrack = TvTrackInfoUtils
.getBestTrackInfo(tracks, id, language, channelCount);
if (bestTrack != null) {
String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO);
if (!bestTrack.getId().equals(selectedTrack)) {
selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack);
} else {
mTvOptionsManager.onMultiAudioChanged(
Utils.getMultiAudioString(this, bestTrack, false));
}
return;
}
mTvOptionsManager.onMultiAudioChanged(null);
}
private void applyClosedCaption() {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks == null) {
mTvOptionsManager.onClosedCaptionsChanged(null);
return;
}
boolean enabled = mCaptionSettings.isEnabled();
mTvView.setClosedCaptionEnabled(enabled);
String selectedTrackId = getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE);
TvTrackInfo alternativeTrack = null;
if (enabled) {
String language = mCaptionSettings.getLanguage();
String trackId = mCaptionSettings.getTrackId();
for (TvTrackInfo track : tracks) {
if (Utils.isEqualLanguage(track.getLanguage(), language)) {
if (track.getId().equals(trackId)) {
if (!track.getId().equals(selectedTrackId)) {
selectTrack(TvTrackInfo.TYPE_SUBTITLE, track);
} else {
// Already selected. Update the option string only.
mTvOptionsManager.onClosedCaptionsChanged(track);
}
if (DEBUG) {
Log.d(TAG, "Subtitle Track Selected {id=" + track.getId()
+ ", language=" + track.getLanguage() + "}");
}
return;
} else if (alternativeTrack == null) {
alternativeTrack = track;
}
}
}
if (alternativeTrack != null) {
if (!alternativeTrack.getId().equals(selectedTrackId)) {
selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack);
} else {
mTvOptionsManager.onClosedCaptionsChanged(alternativeTrack);
}
if (DEBUG) {
Log.d(TAG, "Subtitle Track Selected {id=" + alternativeTrack.getId()
+ ", language=" + alternativeTrack.getLanguage() + "}");
}
return;
}
}
if (selectedTrackId != null) {
selectTrack(TvTrackInfo.TYPE_SUBTITLE, null);
if (DEBUG) Log.d(TAG, "Subtitle Track Unselected");
return;
}
mTvOptionsManager.onClosedCaptionsChanged(null);
}
/**
* Pops up the KeypadChannelSwitchView with the given key input event.
*
* @param keyCode A key code of the key event.
*/
public void showKeypadChannelSwitchView(int keyCode) {
if (mChannelTuner.areAllChannelsLoaded()) {
mOverlayManager.showKeypadChannelSwitch();
mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
}
}
public void showSearchActivity() {
// HACK: Once we moved the window layer to TYPE_APPLICATION_SUB_PANEL,
// the voice button doesn't work. So we directly call the voice action.
SearchManagerHelper.getInstance(this).launchAssistAction();
}
public void showProgramGuideSearchFragment() {
getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment)
.addToBackStack(null).commit();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// Do not save instance state because restoring instance state when TV app died
// unexpectedly can cause some problems like initializing fragments duplicately and
// accessing resource before it is initialized.
}
@Override
protected void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy()");
if (mChannelTuner != null) {
mChannelTuner.removeListener(mChannelTunerListener);
mChannelTuner.stop();
}
TvApplication application = ((TvApplication) getApplication());
if (mProgramDataManager != null) {
mProgramDataManager.removeOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
if (application.getMainActivityWrapper().isCurrent(this)) {
mProgramDataManager.setPrefetchEnabled(false);
}
}
if (mPipInputManager != null) {
mPipInputManager.stop();
}
if (mOverlayManager != null) {
mOverlayManager.release();
}
if (mKeypadChannelSwitchView != null) {
mKeypadChannelSwitchView.setChannels(null);
}
mMemoryManageables.clear();
if (mMediaSession != null) {
mMediaSession.release();
}
if (mAudioCapabilitiesReceiver != null) {
mAudioCapabilitiesReceiver.unregister();
}
mHandler.removeCallbacksAndMessages(null);
application.getMainActivityWrapper().onMainActivityDestroyed(this);
if (mSendConfigInfoRecurringRunner != null) {
mSendConfigInfoRecurringRunner.stop();
mSendConfigInfoRecurringRunner = null;
}
if (mChannelStatusRecurringRunner != null) {
mChannelStatusRecurringRunner.stop();
mChannelStatusRecurringRunner = null;
}
if (mTvInputManagerHelper != null) {
mTvInputManagerHelper.removeCallback(mTvInputCallback);
}
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "onKeyDown(" + keyCode + ", " + event + ")");
}
switch (mOverlayManager.onKeyDown(keyCode, event)) {
case KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY:
return super.onKeyDown(keyCode, event);
case KEY_EVENT_HANDLER_RESULT_HANDLED:
return true;
case KEY_EVENT_HANDLER_RESULT_NOT_HANDLED:
return false;
case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH:
default:
// pass through
}
if (mSearchFragment.isVisible()) {
return super.onKeyDown(keyCode, event);
}
if (!mChannelTuner.areAllChannelsLoaded()) {
return false;
}
if (!mChannelTuner.isCurrentChannelPassthrough()) {
switch (keyCode) {
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
moveToAdjacentChannel(true, false);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED,
System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
mTracker.sendChannelUp();
}
return true;
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
moveToAdjacentChannel(false, false);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED,
System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
mTracker.sendChannelDown();
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
/*
* The following keyboard keys map to these remote keys or "debug actions"
* - --------
* A KEYCODE_MEDIA_AUDIO_TRACK
* D debug: show debug options
* E updateChannelBannerAndShowIfNeeded
* I KEYCODE_TV_INPUT
* O debug: show display mode option
* P debug: togglePipView
* S KEYCODE_CAPTIONS: select subtitle
* W debug: toggle screen size
* V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
* X KEYCODE_BUTTON_X KEYCODE_PROG_BLUE debug: record current channel for a few minutes
* Y KEYCODE_BUTTON_Y KEYCODE_PROG_GREEN debug: Play a recording
*/
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")");
}
// If we are in the middle of channel change, finish it before showing overlays.
finishChannelChangeIfNeeded();
if (event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) {
showSearchActivity();
return true;
}
switch (mOverlayManager.onKeyUp(keyCode, event)) {
case KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY:
return super.onKeyUp(keyCode, event);
case KEY_EVENT_HANDLER_RESULT_HANDLED:
return true;
case KEY_EVENT_HANDLER_RESULT_NOT_HANDLED:
return false;
case KEY_EVENT_HANDLER_RESULT_PASSTHROUGH:
default:
// pass through
}
if (mSearchFragment.isVisible()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
getFragmentManager().popBackStack();
return true;
}
return super.onKeyUp(keyCode, event);
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
// When the event is from onUnhandledInputEvent, onBackPressed is not automatically
// called. Therefore, we need to explicitly call onBackPressed().
onBackPressed();
return true;
}
if (!mChannelTuner.areAllChannelsLoaded()) {
// Now channel map is under loading.
} else if (mChannelTuner.getBrowsableChannelCount() == 0) {
switch (keyCode) {
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_E:
case KeyEvent.KEYCODE_MENU:
showSettingsFragment();
return true;
}
} else {
if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
showKeypadChannelSwitchView(keyCode);
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (!PermissionUtils.hasModifyParentalControls(this)) {
// TODO: support this feature for non-system LC app. b/23939816
return true;
}
PinDialogFragment dialog = null;
if (mTvView.isScreenBlocked()) {
dialog = new PinDialogFragment(
PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL,
new PinDialogFragment.ResultListener() {
@Override
public void done(boolean success) {
if (success) {
unblockScreen(mTvView);
mIsCurrentChannelUnblockedByUser = true;
}
}
});
} else if (mTvView.getBlockedContentRating() != null) {
final TvContentRating rating = mTvView.getBlockedContentRating();
dialog = new PinDialogFragment(
PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
new PinDialogFragment.ResultListener() {
@Override
public void done(boolean success) {
if (success) {
mLastAllowedRatingForCurrentChannel = rating;
mTvView.unblockContent(rating);
}
}
});
}
if (dialog != null) {
mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog,
false);
}
return true;
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
case KeyEvent.KEYCODE_E:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_MENU:
if (event.isCanceled()) {
// Ignore canceled key.
// Note that if there's a TIS granted RECEIVE_INPUT_EVENT,
// fallback keys not blacklisted will have FLAG_CANCELED.
// See dispatchKeyEvent() for detail.
return true;
}
if (keyCode != KeyEvent.KEYCODE_MENU) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
}
if (keyCode != KeyEvent.KEYCODE_E) {
mOverlayManager.showMenu(mTvView.isRecordingPlayback()
? Menu.REASON_RECORDING_PLAYBACK : Menu.REASON_NONE);
}
return true;
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
// Channel change is already done in the head of this method.
return true;
case KeyEvent.KEYCODE_S:
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
case KeyEvent.KEYCODE_CAPTIONS: {
mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
return true;
}
case KeyEvent.KEYCODE_A:
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment());
return true;
}
case KeyEvent.KEYCODE_GUIDE: {
mOverlayManager.showProgramGuide();
return true;
}
case KeyEvent.KEYCODE_INFO: {
mOverlayManager.showBanner();
return true;
}
}
}
if (SystemProperties.USE_DEBUG_KEYS.getValue()) {
switch (keyCode) {
case KeyEvent.KEYCODE_W: {
mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
if (mDebugNonFullSizeScreen) {
FrameLayout.LayoutParams params =
(FrameLayout.LayoutParams) mTvView.getLayoutParams();
params.width = 960;
params.height = 540;
params.gravity = Gravity.START;
mTvView.setLayoutParams(params);
} else {
FrameLayout.LayoutParams params =
(FrameLayout.LayoutParams) mTvView.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER;
mTvView.setLayoutParams(params);
}
return true;
}
case KeyEvent.KEYCODE_P: {
togglePipView();
return true;
}
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT: {
mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
return true;
}
case KeyEvent.KEYCODE_O: {
mOverlayManager.getSideFragmentManager().show(new DisplayModeFragment());
return true;
}
case KeyEvent.KEYCODE_D:
mOverlayManager.getSideFragmentManager().show(new DebugOptionFragment());
return true;
case KeyEvent.KEYCODE_MEDIA_RECORD: // TODO(DVR) handle with debug_keys set
case KeyEvent.KEYCODE_V: {
DvrManager dvrManager = TvApplication.getSingletons(this).getDvrManager();
long startTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5);
long endTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35);
dvrManager.addSchedule(getCurrentChannel(), startTime, endTime);
return true;
}
case KeyEvent.KEYCODE_PROG_BLUE:
case KeyEvent.KEYCODE_BUTTON_X:
case KeyEvent.KEYCODE_X: {
if (CommonFeatures.DVR.isEnabled(this)) {
Channel channel = mTvView.getCurrentChannel();
long channelId = channel.getId();
Program p = mProgramDataManager.getCurrentProgram(channelId);
if (p == null) {
long now = System.currentTimeMillis();
mDvrManager
.addSchedule(channel, now, now + TimeUnit.MINUTES.toMillis(1));
} else {
mDvrManager.addSchedule(p,
mDvrManager.getScheduledRecordingsThatConflict(p));
}
return true;
}
}
case KeyEvent.KEYCODE_PROG_YELLOW:
case KeyEvent.KEYCODE_BUTTON_Y:
case KeyEvent.KEYCODE_Y: {
if (CommonFeatures.DVR.isEnabled(this) && BuildCompat.isAtLeastN()) {
// TODO(DVR) only get finished recordings.
List<RecordedProgram> recordedPrograms = mDvrDataManager
.getRecordedPrograms();
Log.d(TAG, "Found " + recordedPrograms.size() + " recordings");
if (recordedPrograms.isEmpty()) {
Toast.makeText(this, "No finished recording to play", Toast.LENGTH_LONG)
.show();
} else {
RecordedProgram r = recordedPrograms.get(0);
Intent intent = new Intent(this, DvrPlayActivity.class);
intent.putExtra(ScheduledRecording.RECORDING_ID_EXTRA, r.getId());
startActivity(intent);
}
return true;
}
}
}
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event);
if (USE_BACK_KEY_LONG_PRESS) {
// Treat the BACK key long press as the normal press since we changed the behavior in
// onBackPressed().
if (keyCode == KeyEvent.KEYCODE_BACK) {
// It takes long time for TV app to finish, so stop TV first.
stopAll(false);
super.onBackPressed();
return true;
}
}
return false;
}
@Override
public void onBackPressed() {
// The activity should be returned to the caller of this activity
// when the mSource is not null.
if (!mOverlayManager.getSideFragmentManager().isActive() && isPlaying()
&& mSource == null) {
// If back key would exit TV app,
// show McLauncher instead so we can get benefit of McLauncher's shyMode.
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startActivity(startMain);
} else {
super.onBackPressed();
}
}
@Override
public void onUserInteraction() {
super.onUserInteraction();
if (mOverlayManager != null) {
mOverlayManager.onUserInteraction();
}
}
@Override
public void enterPictureInPictureMode() {
// We need to hide overlay first, before moving the activity to PIP. If not, UI will
// be shown during PIP stack resizing, because UI and its animation is stuck during
// PIP resizing.
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
mHandler.post(new Runnable() {
@Override
public void run() {
MainActivity.super.enterPictureInPictureMode();
}
});
}
public void togglePipView() {
enablePipView(!mPipEnabled, true);
mOverlayManager.getMenu().update();
}
public boolean isPipEnabled() {
return mPipEnabled;
}
public void tuneToChannelForPip(Channel channel) {
if (!mPipEnabled) {
throw new IllegalStateException("tuneToChannelForPip is called when PIP is off");
}
if (mPipChannel.equals(channel)) {
return;
}
mPipChannel = channel;
startPip(true);
}
public void enablePipView(boolean enable, boolean fromUserInteraction) {
if (enable == mPipEnabled) {
return;
}
if (enable) {
List<PipInput> pipAvailableInputs = mPipInputManager.getPipInputList(true);
if (pipAvailableInputs.isEmpty()) {
Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
.show();
return;
}
// TODO: choose the last pip input.
Channel pipChannel = pipAvailableInputs.get(0).getChannel();
if (pipChannel != null) {
mPipEnabled = true;
mPipChannel = pipChannel;
startPip(fromUserInteraction);
mTvViewUiManager.restorePipSize();
mTvViewUiManager.restorePipLayout();
mTvOptionsManager.onPipChanged(mPipEnabled);
} else {
Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
.show();
}
} else {
mPipEnabled = false;
mPipChannel = null;
// Recover the stream volume of the main TV view, if needed.
if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
setVolumeByAudioFocusStatus(mTvView);
mPipSound = TvSettings.PIP_SOUND_MAIN;
mTvOptionsManager.onPipSoundChanged(mPipSound);
}
stopPip();
mTvViewUiManager.restoreDisplayMode(false);
mTvOptionsManager.onPipChanged(mPipEnabled);
}
}
private boolean isChannelChangeKeyDownReceived() {
return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED)
|| mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED);
}
private void finishChannelChangeIfNeeded() {
if (!isChannelChangeKeyDownReceived()) {
return;
}
mHandler.removeMessages(MSG_CHANNEL_UP_PRESSED);
mHandler.removeMessages(MSG_CHANNEL_DOWN_PRESSED);
if (mChannelTuner.getBrowsableChannelCount() > 0) {
if (!mTvView.isPlaying()) {
// We expect that mTvView is already played. But, it is sometimes not.
// TODO: we figure out the reason when mTvView is not played.
Log.w(TAG, "TV view isn't played in finishChannelChangeIfNeeded");
}
tuneToChannel(mChannelTuner.getCurrentChannel());
} else {
showSettingsFragment();
}
}
private boolean dispatchKeyEventToSession(final KeyEvent event) {
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
}
if (mPipEnabled && mChannelTuner.isCurrentChannelPassthrough()) {
// If PIP is enabled, key events will be used by UI.
return false;
}
boolean handled = false;
if (mTvView != null) {
handled = mTvView.dispatchKeyEvent(event);
}
if (isKeyEventBlocked()) {
if ((event.getKeyCode() == KeyEvent.KEYCODE_BACK
|| event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_B) && mNeedShowBackKeyGuide) {
// KeyEvent.KEYCODE_BUTTON_B is also used like the back button.
Toast.makeText(this, R.string.msg_back_key_guide, Toast.LENGTH_SHORT).show();
mNeedShowBackKeyGuide = false;
}
return true;
}
return handled;
}
private boolean isKeyEventBlocked() {
// If the current channel is passthrough channel without a PIP view,
// we always don't handle the key events in TV activity. Instead, the key event will
// be handled by the passthrough TV input.
return mChannelTuner.isCurrentChannelPassthrough() && !mPipEnabled;
}
public void tuneToLastWatchedChannelForTunerInput() {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
return;
}
if (mPipEnabled) {
if (!mPipChannel.isPassthrough()) {
enablePipView(false, true);
}
}
stopTv();
startTv(null);
}
public void tuneToChannel(Channel channel) {
if (channel == null) {
if (mTvView.isPlaying()) {
mTvView.reset();
}
} else {
if (mPipEnabled && mPipInputManager.areInSamePipInput(channel, mPipChannel)) {
enablePipView(false, true);
}
if (!mTvView.isPlaying()) {
startTv(channel.getUri());
} else if (channel.equals(mTvView.getCurrentChannel())) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
} else if (mChannelTuner.moveToChannel(channel)) {
// Channel banner would be updated inside of tune.
tune();
} else {
showSettingsFragment();
}
}
}
/**
* This method just moves the channel in the channel map and updates the channel banner,
* but doesn't actually tune to the channel.
* The caller of this method should call {@link #tune} in the end.
*
* @param channelUp {@code true} for channel up, and {@code false} for channel down.
* @param fastTuning {@code true} if fast tuning is requested.
*/
private void moveToAdjacentChannel(boolean channelUp, boolean fastTuning) {
if (mChannelTuner.moveToAdjacentBrowsableChannel(channelUp)) {
updateChannelBannerAndShowIfNeeded(fastTuning ? UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST
: UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
}
public Channel getPipChannel() {
return mPipChannel;
}
/**
* Swap the main and the sub screens while in the PIP mode.
*/
public void swapPip() {
if (!mPipEnabled || mTvView == null || mPipView == null) {
Log.e(TAG, "swapPip() - not in PIP");
mPipSwap = false;
return;
}
Channel channel = mTvView.getCurrentChannel();
boolean tvViewBlocked = mTvView.isScreenBlocked();
boolean pipViewBlocked = mPipView.isScreenBlocked();
if (channel == null || !mTvView.isPlaying()) {
// If the TV view is not currently playing or its current channel is null, swapping here
// basically means disabling the PIP mode and getting back to the full screen since
// there's no point of keeping a blank PIP screen at the bottom which is not tune-able.
enablePipView(false, true);
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT);
mPipSwap = false;
return;
}
// Reset the TV view and tune the PIP view to the previous channel of the TV view.
mTvView.reset();
mPipView.reset();
Channel oldPipChannel = mPipChannel;
tuneToChannelForPip(channel);
if (tvViewBlocked) {
mPipView.blockScreen();
} else {
mPipView.unblockScreen();
}
if (oldPipChannel != null) {
// Tune the TV view to the previous PIP channel.
tuneToChannel(oldPipChannel);
}
if (pipViewBlocked) {
mTvView.blockScreen();
} else {
mTvView.unblockScreen();
}
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
setVolumeByAudioFocusStatus(mTvView);
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
setVolumeByAudioFocusStatus(mPipView);
}
mPipSwap = !mPipSwap;
mTvOptionsManager.onPipSwapChanged(mPipSwap);
}
/**
* Toggle where the sound is coming from when the user is watching the PIP.
*/
public void togglePipSoundMode() {
if (!mPipEnabled || mTvView == null || mPipView == null) {
Log.e(TAG, "togglePipSoundMode() - not in PIP");
return;
}
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
setVolumeByAudioFocusStatus(mPipView);
mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW;
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
setVolumeByAudioFocusStatus(mTvView);
mPipSound = TvSettings.PIP_SOUND_MAIN;
}
restoreMainTvView();
mTvOptionsManager.onPipSoundChanged(mPipSound);
}
/**
* Set the main TV view which holds HDMI-CEC active source based on the sound mode
*/
private void restoreMainTvView() {
if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
mTvView.setMain();
} else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
mPipView.setMain();
}
}
@Override
public void onVisibleBehindCanceled() {
stopTv("onVisibleBehindCanceled()", false);
mTracker.sendScreenView("");
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
mMediaSession.setActive(false);
}
stopPip();
mVisibleBehind = false;
if (!mOtherActivityLaunched && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
// Workaround: in M, onStop is not called, even though it should be called after
// onVisibleBehindCanceled is called. As a workaround, we call finish().
finish();
}
super.onVisibleBehindCanceled();
}
@Override
public void startActivity(Intent intent) {
mOtherActivityLaunched = true;
super.startActivity(intent);
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
mOtherActivityLaunched = true;
super.startActivityForResult(intent, requestCode);
}
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
public String getSelectedTrack(int type) {
return mTvView.getSelectedTrack(type);
}
public void selectTrack(int type, TvTrackInfo track) {
mTvView.selectTrack(type, track == null ? null : track.getId());
if (type == TvTrackInfo.TYPE_AUDIO) {
mTvOptionsManager.onMultiAudioChanged(track == null ? null :
Utils.getMultiAudioString(this, track, false));
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
mTvOptionsManager.onClosedCaptionsChanged(track);
}
}
public void selectAudioTrack(String trackId) {
saveMultiAudioSetting(trackId);
applyMultiAudio();
}
private void saveMultiAudioSetting(String trackId) {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks != null) {
for (TvTrackInfo track : tracks) {
if (track.getId().equals(trackId)) {
TvSettings.setMultiAudioId(this, track.getId());
TvSettings.setMultiAudioLanguage(this, track.getLanguage());
TvSettings.setMultiAudioChannelCount(this, track.getAudioChannelCount());
return;
}
}
}
TvSettings.setMultiAudioId(this, null);
TvSettings.setMultiAudioLanguage(this, null);
TvSettings.setMultiAudioChannelCount(this, 0);
}
public void selectSubtitleTrack(int option, String trackId) {
saveClosedCaptionSetting(option, trackId);
applyClosedCaption();
}
public void selectSubtitleLanguage(int option, String language, String trackId) {
mCaptionSettings.setEnableOption(option);
mCaptionSettings.setLanguage(language);
mCaptionSettings.setTrackId(trackId);
applyClosedCaption();
}
private void saveClosedCaptionSetting(int option, String trackId) {
mCaptionSettings.setEnableOption(option);
if (option == CaptionSettings.OPTION_ON) {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks != null) {
for (TvTrackInfo track : tracks) {
if (track.getId().equals(trackId)) {
mCaptionSettings.setLanguage(track.getLanguage());
mCaptionSettings.setTrackId(trackId);
return;
}
}
}
}
}
private void updateAvailabilityToast() {
updateAvailabilityToast(mTvView);
}
private void updateAvailabilityToast(StreamInfo info) {
if (info.isVideoAvailable()) {
return;
}
int stringId;
switch (info.getVideoUnavailableReason()) {
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
stringId = R.string.msg_channel_unavailable_unknown;
break;
}
Toast.makeText(this, stringId, Toast.LENGTH_SHORT).show();
}
public ParentalControlSettings getParentalControlSettings() {
return mTvInputManagerHelper.getParentalControlSettings();
}
/**
* Returns a ContentRatingsManager instance.
*/
public ContentRatingsManager getContentRatingsManager() {
return mTvInputManagerHelper.getContentRatingsManager();
}
public CaptionSettings getCaptionSettings() {
return mCaptionSettings;
}
// Initialize TV app for test. The setup process should be finished before the Live TV app is
// started. We only enable all the channels here.
private void initForTest() {
if (!TvCommonUtils.isRunningInTest()) {
return;
}
Utils.enableAllChannels(this);
}
// Lazy initialization
private void lazyInitializeIfNeeded(long delay) {
// Already initialized.
if (mLazyInitialized) {
return;
}
mLazyInitialized = true;
// Running initialization.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
initAnimations();
initSideFragments();
}
}, delay);
}
private void initAnimations() {
mTvViewUiManager.initAnimatorIfNeeded();
mOverlayManager.initAnimatorIfNeeded();
}
private void initSideFragments() {
SideFragment.preloadRecycledViews(this);
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
for (MemoryManageable memoryManageable : mMemoryManageables) {
memoryManageable.performTrimMemory(level);
}
}
private static class MainActivityHandler extends WeakHandler<MainActivity> {
MainActivityHandler(MainActivity mainActivity) {
super(mainActivity);
}
@Override
protected void handleMessage(Message msg, @NonNull MainActivity mainActivity) {
switch (msg.what) {
case MSG_CHANNEL_DOWN_PRESSED:
long startTime = (Long) msg.obj;
mainActivity.moveToAdjacentChannel(false, true);
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
break;
case MSG_CHANNEL_UP_PRESSED:
startTime = (Long) msg.obj;
mainActivity.moveToAdjacentChannel(true, true);
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
break;
case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE:
mainActivity.updateChannelBannerAndShowIfNeeded(
UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
break;
}
}
private long getDelay(long startTime) {
if (System.currentTimeMillis() - startTime > CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS) {
return CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED;
}
return CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED;
}
}
private class MyOnTuneListener implements OnTuneListener {
boolean mUnlockAllowedRatingBeforeShrunken = true;
boolean mWasUnderShrunkenTvView;
long mStreamInfoUpdateTimeThresholdMs;
Channel mChannel;
public MyOnTuneListener() { }
private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
mStreamInfoUpdateTimeThresholdMs =
System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
mChannel = channel;
mWasUnderShrunkenTvView = wasUnderShrukenTvView;
}
private void onPlayRecording() {
mStreamInfoUpdateTimeThresholdMs =
System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
mChannel = null;
mWasUnderShrunkenTvView = false;
}
@Override
public void onUnexpectedStop(Channel channel) {
stopTv();
startTv(null);
}
@Override
public void onTuneFailed(Channel channel) {
Log.w(TAG, "Failed to tune to channel " + channel.getId()
+ "@" + channel.getInputId());
if (mTvView.isFadedOut()) {
mTvView.removeFadeEffect();
}
// TODO: show something to user about this error.
}
@Override
public void onStreamInfoChanged(StreamInfo info) {
if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
mTracker.sendChannelTuneTime(info.getCurrentChannel(),
mTuneDurationTimer.reset());
}
// If updateChannelBanner() is called without delay, the stream info seems flickering
// when the channel is quickly changed.
if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE)
&& info.isVideoAvailable()) {
if (System.currentTimeMillis() > mStreamInfoUpdateTimeThresholdMs) {
updateChannelBannerAndShowIfNeeded(
UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
} else {
mHandler.sendMessageDelayed(mHandler.obtainMessage(
MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE),
mStreamInfoUpdateTimeThresholdMs - System.currentTimeMillis());
}
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvView();
applyMultiAudio();
applyClosedCaption();
// TODO: Send command to TIS with checking the settings in TV and CaptionManager.
mOverlayManager.getMenu().onStreamInfoChanged();
if (mTvView.isVideoAvailable()) {
mTvViewUiManager.fadeInTvView();
}
mHandler.removeCallbacks(mRestoreMainViewRunnable);
restoreMainTvView();
}
@Override
public void onChannelRetuned(Uri channel) {
if (channel == null) {
return;
}
Channel currentChannel =
mChannelDataManager.getChannel(ContentUris.parseId(channel));
if (currentChannel == null) {
Log.e(TAG, "onChannelRetuned is called but can't find a channel with the URI "
+ channel);
return;
}
if (isChannelChangeKeyDownReceived()) {
// Ignore this message if the user is changing the channel.
return;
}
mChannelTuner.setCurrentChannel(currentChannel);
mTvView.setCurrentChannel(currentChannel);
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
@Override
public void onContentBlocked() {
mTuneDurationTimer.reset();
TvContentRating rating = mTvView.getBlockedContentRating();
// When tuneTo was called while TV view was shrunken, if the channel id is the same
// with the channel watched before shrunken, we allow the rating which was allowed
// before.
if (mWasUnderShrunkenTvView && mUnlockAllowedRatingBeforeShrunken
&& mChannelBeforeShrunkenTvView.equals(mChannel)
&& rating.equals(mAllowedRatingBeforeShrunken)) {
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
}
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
mTvViewUiManager.fadeInTvView();
}
@Override
public void onContentAllowed() {
if (!isUnderShrunkenTvView()) {
mUnlockAllowedRatingBeforeShrunken = false;
}
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
}
}
}