blob: 8dbafe476f80cb2a1dd72abaf0798f415fc54340 [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 static com.android.tv.common.feature.SystemAppFeature.SYSTEM_APP_FEATURE;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.SearchManager;
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.content.res.Configuration;
import android.database.Cursor;
import android.hardware.display.DisplayManager;
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.BaseColumns;
import android.provider.Settings;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
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.ViewTreeObserver;
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.MainActivity.MySingletons;
import com.android.tv.analytics.Tracker;
import com.android.tv.audio.AudioManagerHelper;
import com.android.tv.audiotvservice.AudioOnlyTvServiceUtil;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.CommonConstants;
import com.android.tv.common.CommonPreferences;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.compat.TvInputInfoCompat;
import com.android.tv.common.dev.DeveloperPreferences;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.common.memory.MemoryManageable;
import com.android.tv.common.singletons.HasSingletons;
import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.ContentUriUtils;
import com.android.tv.common.util.Debug;
import com.android.tv.common.util.DurationTimer;
import com.android.tv.common.util.PermissionUtils;
import com.android.tv.common.util.SystemProperties;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ChannelImpl;
import com.android.tv.data.OnCurrentProgramUpdatedListener;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.ProgramImpl;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.api.Channel;
import com.android.tv.data.api.Program;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.ConflictChecker;
import com.android.tv.dvr.ui.DvrAlreadyRecordedFragment;
import com.android.tv.dvr.ui.DvrAlreadyScheduledFragment;
import com.android.tv.dvr.ui.DvrScheduleFragment;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.features.TvFeatures;
import com.android.tv.guide.ProgramItemView;
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.perf.StartupMeasureFactory;
import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.search.ProgramGuideSearchFragment;
import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
import com.android.tv.ui.ChannelBannerView;
import com.android.tv.ui.DetailsActivity;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
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.BlockScreenType;
import com.android.tv.ui.TunableTvView.OnTuneListener;
import com.android.tv.ui.TvOverlayManager;
import com.android.tv.ui.TvOverlayManagerFactory;
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.DeveloperOptionFragment;
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.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.CaptionSettings;
import com.android.tv.util.GtvUtils;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
import com.android.tv.util.ViewCache;
import com.android.tv.util.account.AccountHelper;
import com.android.tv.util.images.ImageCache;
import com.google.common.base.Optional;
import dagger.android.AndroidInjection;
import dagger.android.AndroidInjector;
import dagger.android.ContributesAndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasAndroidInjector;
import com.android.tv.common.flags.BackendKnobsFlags;
import com.android.tv.common.flags.LegacyFlags;
import com.android.tv.common.flags.StartupFlags;
import com.android.tv.common.flags.UiFlags;
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.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Provider;
/** The main activity for the TV app. */
public class MainActivity extends Activity
implements OnActionClickListener,
OnPinCheckedListener,
ChannelChanger,
HasSingletons<MySingletons>,
HasAndroidInjector {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
/** Singletons needed for this class. */
public interface MySingletons extends ChannelBannerView.MySingletons {}
@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 FRAME_RATE_FOR_FILM = 23.976f;
private static final float FRAME_RATE_EPSILON = 0.1f;
// AOSP_Comment_Out private static final String PLUTO_TV_PACKAGE_NAME = "tv.pluto.android";
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
// Tracker screen names.
public static final String SCREEN_NAME = "Main";
private static final String SCREEN_PIP = "PIP";
private static final String SCREEN_BEHIND_NAME = "Behind";
private static final float REFRESH_RATE_EPSILON = 0.01f;
private static final HashSet<Integer> BLOCKLIST_KEYCODE_TO_TIS;
// These keys won't be passed to TIS in addition to gamepad buttons.
static {
BLOCKLIST_KEYCODE_TO_TIS = new HashSet<>();
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_TV_INPUT);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MENU);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_CHANNEL_UP);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_CHANNEL_DOWN);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_UP);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_DOWN);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_MUTE);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MUTE);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_SEARCH);
BLOCKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_WINDOW);
}
private static final IntentFilter SYSTEM_INTENT_FILTER = new IntentFilter();
static {
SYSTEM_INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_ON);
SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_TIME_CHANGED);
}
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
private static final int REQUEST_CODE_NOW_PLAYING = 2;
private static final String KEY_INIT_CHANNEL_ID = "com.android.tv.init_channel_id";
// 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 MSG_CHANNEL_DOWN_PRESSED = 1000;
private static final int MSG_CHANNEL_UP_PRESSED = 1001;
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 static final int UNDEFINED_TRACK_INDEX = -1;
private static final int HIGHEST_PRIORITY = -1;
private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
{
StartupMeasureFactory.create().onActivityInit();
}
private final MySingletonsImpl mMySingletons = new MySingletonsImpl();
@Inject DispatchingAndroidInjector<Object> mAndroidInjector;
@Inject @DbExecutor Executor mDbExecutor;
private AccessibilityManager mAccessibilityManager;
@Inject ChannelDataManager mChannelDataManager;
@Inject ProgramDataManager mProgramDataManager;
@Inject TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
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 ConflictChecker mDvrConflictChecker;
@Inject BackendKnobsFlags mBackendKnobs;
@Inject LegacyFlags mLegacyFlags;
@Inject StartupFlags mStartupFlags;
@Inject UiFlags mUiFlags;
@Inject SetupUtils mSetupUtils;
@Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
@Inject AccountHelper mAccountHelper;
@Inject EpgFetcher mEpgFetcher;
@VisibleForTesting protected TunableTvView mTvView;
private View mContentView;
private Bundle mTuneParams;
@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 MediaSessionWrapper mMediaSessionWrapper;
private final MyOnTuneListener mOnTuneListener = new MyOnTuneListener();
private String mInputIdUnderSetup;
private boolean mIsSetupActivityCalledByPopup;
private AudioManagerHelper mAudioManagerHelper;
private boolean mTunePending;
private boolean mDebugNonFullSizeScreen;
private boolean mActivityResumed;
private boolean mActivityStarted;
private boolean mShouldTuneToTunerChannel;
private boolean mUseKeycodeBlocklist;
private boolean mShowLockedChannelsTemporarily;
private boolean mBackKeyPressed;
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
private boolean mShowNewSourcesFragment = true;
private boolean mOtherActivityLaunched;
private boolean mIsInPIPMode;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
@Inject TvOverlayManagerFactory mOverlayFactory;
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 boolean mIsCompletingShrunkenTvView;
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 String mLastInputIdFromIntent;
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
private final BroadcastReceiver mBroadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case 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);
break;
case 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 explicitly.
resumeTvIfNeeded();
}
break;
case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED:
if (DEBUG) Log.d(TAG, "Received parental control settings change");
applyParentalControlSettings();
checkChannelLockNeeded(mTvView, null);
break;
case Intent.ACTION_TIME_CHANGED:
// Re-tune the current channel to prevent incorrect behavior of
// trick-play.
// See: b/37393628
if (mChannelTuner.getCurrentChannel() != null) {
tune(true);
}
break;
default: // fall out
}
}
};
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) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
mMediaSessionWrapper.update(mTvView.isBlocked(), channel, program);
}
}
};
private final ChannelTuner.Listener mChannelTunerListener =
new ChannelTuner.Listener() {
@Override
public void onLoadFinished() {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log("MainActivity.mChannelTunerListener.onLoadFinished");
mSetupUtils.markNewChannelsBrowsable();
if (mActivityResumed) {
resumeTvIfNeeded();
}
mOverlayManager.onBrowsableChannelsUpdated();
}
@Override
public void onBrowsableChannelListChanged() {
mOverlayManager.onBrowsableChannelsUpdated();
}
@Override
public void onCurrentChannelUnavailable(Channel channel) {
if (mChannelTuner.moveToAdjacentBrowsableChannel(true)) {
tune(true);
} else {
stopTv("onCurrentChannelUnavailable()", false);
}
}
@Override
public void onChannelChanged(Channel previousChannel, Channel currentChannel) {
if (currentChannel != null) {
GtvUtils.broadcastInputId(MainActivity.this, currentChannel.getInputId());
}
}
};
private final Runnable mRestoreMainViewRunnable = this::restoreMainTvView;
private ProgramGuideSearchFragment mSearchFragment;
private final TvInputCallback mTvInputCallback =
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
if (mOptionalBuiltInTunerManager.isPresent()
&& CommonPreferences.shouldShowSetupActivity(MainActivity.this)) {
BuiltInTunerManager builtInTunerManager =
mOptionalBuiltInTunerManager.get();
String tunerInputId = builtInTunerManager.getEmbeddedTunerInputId();
if (tunerInputId.equals(inputId)) {
Intent intent =
builtInTunerManager
.getTunerInputController()
.createSetupIntent(MainActivity.this);
startActivity(intent);
CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
mSetupUtils.markAsKnownInput(tunerInputId);
}
}
}
};
private void applyParentalControlSettings() {
boolean parentalControlEnabled =
mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled();
mTvView.onParentalControlChanged(parentalControlEnabled);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ChannelPreviewUpdater.getInstance(this).updatePreviewDataForChannelsImmediately();
}
}
@Override
public MySingletons singletons() {
return mMySingletons;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
if (!startUpDebugTimer.isStarted()
|| startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
// TvApplication can start by other reason before MainActivty is launched.
// In this case, we restart the timer.
startUpDebugTimer.start();
}
startUpDebugTimer.log("MainActivity.onCreate");
if (DEBUG) {
Log.d(TAG, "onCreate()");
}
Starter.start(this);
super.onCreate(savedInstanceState);
if (!mTvInputManagerHelper.hasTvInputManager()) {
Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
finishAndRemoveTask();
return;
}
mAccountHelper.init();
TvSingletons tvApplication = (TvSingletons) getApplication();
// In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
boolean isPassthroughInput =
TvContract.isChannelUriForPassthroughInput(getIntent().getData());
boolean tuneToPassthroughInput =
Intent.ACTION_VIEW.equals(getIntent().getAction()) && isPassthroughInput;
boolean channelLoadedAndNoChannelAvailable =
mChannelDataManager.isDbLoadFinished()
&& mChannelDataManager.getChannelCount() <= 0;
if ((OnboardingUtils.isFirstRunWithCurrentVersion(this)
|| channelLoadedAndNoChannelAvailable)
&& !tuneToPassthroughInput
&& !CommonUtils.isRunningInTest()) {
startOnboardingActivity();
return;
}
setContentView(R.layout.activity_tv);
mTvView = findViewById(R.id.main_tunable_tv_view);
mTvView.initialize(mProgramDataManager, mTvInputManagerHelper, mLegacyFlags);
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;
}
});
mTvView.setBlockedInfoOnClickListener(v -> showPinDialogFragment());
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
if (!isPassthroughInput
&& inputId != null
&& !mStartupFlags.warmupInputidBlacklist().getElementList().contains(inputId)
&& channelId != Channel.INVALID_ID) {
mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId));
}
tvApplication.getMainActivityWrapper().onMainActivityCreated(this);
if (BuildConfig.ENG && DeveloperPreferences.ALLOW_STRICT_MODE.get(this)) {
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.addCallback(mTvInputCallback);
}
mProgramDataManager.addOnCurrentProgramUpdatedListener(
Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper);
mChannelTuner.addListener(mChannelTunerListener);
mChannelTuner.start();
mMemoryManageables.add(mProgramDataManager);
mMemoryManageables.add(ImageCache.getInstance());
mMemoryManageables.add(TvContentRatingCache.getInstance());
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrManager = tvApplication.getDvrManager();
}
mTimeShiftManager =
new TimeShiftManager(
this,
mTvView,
mProgramDataManager,
mTracker,
new OnCurrentProgramUpdatedListener() {
@Override
public void onCurrentProgramUpdated(long channelId, Program program) {
mMediaSessionWrapper.update(
mTvView.isBlocked(), getCurrentChannel(), program);
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:
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager
.UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
break;
case TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY:
default:
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager
.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
break;
}
}
});
DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
mDefaultRefreshRate = display.getRefreshRate();
if (!PermissionUtils.hasAccessWatchedHistory(this)) {
WatchedHistoryManager watchedHistoryManager =
new WatchedHistoryManager(getApplicationContext());
watchedHistoryManager.start();
mTvView.setWatchedHistoryManager(watchedHistoryManager);
}
mTvViewUiManager =
new TvViewUiManager(
this, mTvView, findViewById(android.R.id.content), mTvOptionsManager);
mContentView = findViewById(android.R.id.content);
ViewGroup sceneContainer = findViewById(R.id.scene_container);
ChannelBannerView channelBannerView =
(ChannelBannerView)
getLayoutInflater().inflate(R.layout.channel_banner, sceneContainer, false);
KeypadChannelSwitchView keypadChannelSwitchView =
(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(@NonNull TvInputInfo input) {
Channel currentChannel = mChannelTuner.getCurrentChannel();
String currentInputId =
currentChannel == null ? null : currentChannel.getInputId();
if (TextUtils.equals(input.getId(), currentInputId)) {
hideOverlays();
} else {
tuneToChannel(ChannelImpl.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 =
mOverlayFactory.create(
this,
mChannelTuner,
mTvView,
mTvOptionsManager,
keypadChannelSwitchView,
channelBannerView,
inputBannerView,
selectInputView,
sceneContainer,
mSearchFragment);
mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager);
mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
mAudioCapabilitiesReceiver.register();
Intent nowPlayingIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent);
mTvViewUiManager.restoreDisplayMode(false);
if (!handleIntent(getIntent())) {
finish();
return;
}
if (CommonFeatures.DVR.isEnabled(this)
&& TvFeatures.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) {
mDvrConflictChecker = new ConflictChecker(this);
}
initForTest();
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
}
private void startOnboardingActivity() {
startActivity(OnboardingActivity.buildIntent(this, getIntent()));
finish();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
float density = getResources().getDisplayMetrics().density;
mTvViewUiManager.onConfigurationChanged(
(int) (newConfig.screenWidthDp * density),
(int) (newConfig.screenHeightDp * density));
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Start reload of dependent data
mChannelDataManager.reload();
mProgramDataManager.reload();
// Restart TV app.
Intent intent = getIntent();
finish();
startActivity(intent);
} else {
Toast.makeText(
this,
R.string.msg_read_tv_listing_permission_denied,
Toast.LENGTH_LONG)
.show();
finish();
}
}
}
@BlockScreenType
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) {
if (DEBUG) {
Log.d(TAG, "onNewIntent(): " + intent);
}
if (mOverlayManager == null) {
// It's called before onCreate. The intent will be handled at onCreate. b/30725058
return;
}
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();
registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Intent notificationIntent = new Intent(this, NotificationService.class);
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
}
if (mOptionalBuiltInTunerManager.isPresent()) {
mOptionalBuiltInTunerManager
.get()
.getTunerInputController()
.executeNetworkTunerDiscoveryAsyncTask(this);
}
mEpgFetcher.fetchImmediatelyIfNeeded();
}
@Override
protected void onResume() {
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
mIsInPIPMode = false;
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;
mAudioManagerHelper.requestAudioFocus();
if (mTvView.isPlaying()) {
// Every time onResume() is called the activity will be assumed to not have requested
// visible behind.
requestVisibleBehind(true);
}
Set<String> failedScheduledRecordingInfoSet =
Utils.getFailedScheduledRecordingInfoSet(getApplicationContext());
if (Utils.hasRecordingFailedReason(
getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
&& !failedScheduledRecordingInfoSet.isEmpty()) {
runAfterAttachedToWindow(
() ->
DvrUiHelper.showDvrInsufficientSpaceErrorDialog(
MainActivity.this, failedScheduledRecordingInfoSet));
}
if (mChannelTuner.areAllChannelsLoaded()) {
mSetupUtils.markNewChannelsBrowsable();
resumeTvIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
// NOTE: The following codes are related to pop up an overlay UI after resume. When
// the following code is changed, please modify willShowOverlayUiWhenResume() accordingly.
if (mInputToSetUp != null) {
startSetupActivity(mInputToSetUp, false);
mInputToSetUp = null;
} else if (mShowProgramGuide) {
mShowProgramGuide = false;
// 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.
mHandler.post(() -> mOverlayManager.showProgramGuide());
} else if (mShowSelectInputView) {
mShowSelectInputView = false;
// 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.
mHandler.post(() -> mOverlayManager.showSelectInputView());
}
if (mDvrConflictChecker != null) {
mDvrConflictChecker.start();
}
if (CommonFeatures.ENABLE_TV_SERVICE.isEnabled(this) && isAudioOnlyInput()) {
// TODO(b/110969180): figure out when to call AudioOnlyTvServiceUtil.stopAudioOnlyInput
AudioOnlyTvServiceUtil.startAudioOnlyInput(this, mLastInputIdFromIntent);
}
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
}
@Override
protected void onPause() {
if (DEBUG) Log.d(TAG, "onPause()");
if (mDvrConflictChecker != null) {
mDvrConflictChecker.stop();
}
finishChannelChangeIfNeeded();
mActivityResumed = false;
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT);
mTvView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_NO_UI);
mBackKeyPressed = false;
mShowLockedChannelsTemporarily = false;
mShouldTuneToTunerChannel = false;
if (!mVisibleBehind) {
if (mIsInPIPMode) {
mTracker.sendScreenView(SCREEN_PIP);
} else {
mTracker.sendScreenView("");
mAudioManagerHelper.abandonAudioFocus();
mMediaSessionWrapper.setPlaybackState(false);
}
} 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;
}
@Override
public void onPinChecked(boolean checked, int type, String rating) {
if (checked) {
switch (type) {
case PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
blockOrUnblockScreen(mTvView, false);
mIsCurrentChannelUnblockedByUser = true;
break;
case PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
TvContentRating unblockedRating = TvContentRating.unflattenFromString(rating);
mLastAllowedRatingForCurrentChannel = unblockedRating;
mTvView.unblockContent(unblockedRating);
break;
case PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN:
mOverlayManager
.getSideFragmentManager()
.show(new ParentalControlsFragment(), false);
// fall through.
case PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN:
mOverlayManager.getSideFragmentManager().showSidePanel(true);
break;
default: // fall out
}
} else if (type == PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN) {
mOverlayManager.getSideFragmentManager().hideAll(false);
}
}
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.");
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 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 || channelUri.equals(mChannelTuner.getCurrentChannelUri())) {
// Simply adjust the volume without tune.
mAudioManagerHelper.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)) {
ChannelImpl channel = ChannelImpl.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();
mAudioManagerHelper.setVolumeByAudioFocusStatus();
tune(true);
}
@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();
}
}
if (mChannelTuner.isCurrentChannelPassthrough()) {
mInitChannelUri = mChannelTuner.getCurrentChannelUri();
}
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);
}
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 = CommonUtils.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 TV app
// should go through TV app 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);
try {
startActivitySafe(intent);
} 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 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;
}
/** Returns the {@link ConflictChecker}. */
@Nullable
public ConflictChecker getDvrConflictChecker() {
return mDvrConflictChecker;
}
public Channel getCurrentChannel() {
return mChannelTuner.getCurrentChannel();
}
public long getCurrentChannelId() {
return mChannelTuner.getCurrentChannelId();
}
/**
* Returns the current program which the user is watching right now.
*
* <p>It might be a live program. If the time shifting is available, it can be a past program,
* too.
*/
public Program getCurrentProgram() {
if (!isChannelChangeKeyDownReceived() && mTimeShiftManager.isAvailable()) {
// We shouldn't get current program from TimeShiftManager during channel tunning
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();
}
private Channel getBrowsableChannel() {
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);
}
/** Show settings fragment. */
public void showSettingsFragment() {
if (!mChannelTuner.areAllChannelsLoaded()) {
// Show ChannelSourcesFragment only if all the channels are loaded.
return;
}
mOverlayManager.getSideFragmentManager().show(new SettingsFragment());
}
public void showMerchantCollection() {
Intent onlineStoreIntent = OnboardingUtils.createOnlineStoreIntent(mUiFlags);
if (onlineStoreIntent != null) {
startActivitySafe(onlineStoreIntent);
} else {
Log.w(
TAG,
"Unable to show merchant collection, more channels url is not valid. url is "
+ mUiFlags.moreChannelsUrl());
}
}
/**
* 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;
mTvViewUiManager.startShrunkenTvView();
if (showLockedChannelsTemporarily) {
mShowLockedChannelsTemporarily = true;
checkChannelLockNeeded(mTvView, null);
}
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 =
() -> {
tuneToChannel(channel);
if (mChannelBeforeShrunkenTvView == null
|| !mChannelBeforeShrunkenTvView.equals(channel)) {
Utils.setLastWatchedChannel(MainActivity.this, channel);
}
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
};
mTvViewUiManager.fadeOutTvView(tuneAction);
// Will automatically fade-in when video becomes available.
} else {
checkChannelLockNeeded(mTvView, null);
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
}
}
private boolean isUnderShrunkenTvView() {
return mTvViewUiManager.isUnderShrunkenTvView() || mIsCompletingShrunkenTvView;
}
/**
* Returns {@code true} if the tunable tv view is blocked by resource conflict or by parental
* control, otherwise {@code false}.
*/
public boolean isScreenBlockedByResourceConflictOrParentalControl() {
return mTvView.getVideoUnavailableReason()
== TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE
|| mTvView.isBlocked();
}
@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(true);
}
} else {
mInputIdUnderSetup = null;
}
if (!mIsSetupActivityCalledByPopup) {
mOverlayManager.getSideFragmentManager().showSidePanel(false);
}
break;
case REQUEST_CODE_NOW_PLAYING:
// nothing needs to be done. onResume will restore everything.
break;
default:
// do nothing
}
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 boolean dispatchKeyEvent(KeyEvent event) {
if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
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 ((mContentView.hasFocusable() && !mOverlayManager.getSideFragmentManager().isHiding())
|| mOverlayManager.getSideFragmentManager().isActive()) {
return super.dispatchKeyEvent(event);
}
if (BLOCKLIST_KEYCODE_TO_TIS.contains(event.getKeyCode())
|| KeyEvent.isGamepadButton(event.getKeyCode())) {
// If the event is in blocklisted or gamepad key, do not pass it to session.
// Gamepad keys are blocklisted 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,
// blocklist 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);
}
/** Notifies the key input focus is changed to the TV view. */
public void updateKeyInputFocus() {
mHandler.post(() -> mTvView.setBlockScreenType(getDesiredBlockScreenType()));
}
// It should be called before onResume.
private boolean handleIntent(Intent intent) {
mLastInputIdFromIntent = getInputId(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);
}
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 (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
runAfterAttachedToWindow(() -> mOverlayManager.showSetupFragment());
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
if (Utils.isProgramsUri(uri)) {
// 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.
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();
String programUriString = intent.getStringExtra(SearchManager.EXTRA_DATA_KEY);
Uri programUriFromIntent =
programUriString == null ? null : Uri.parse(programUriString);
long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri);
if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) {
new AsyncQueryProgramTask(
mDbExecutor,
programUriFromIntent,
ProgramImpl.PROJECTION,
null,
null,
null,
channelIdFromIntent)
.executeOnDbThread();
}
if (mTuneParams == null) {
mTuneParams = new Bundle();
}
if (Utils.isChannelUriForTunerInput(mInitChannelUri)) {
mTuneParams.putLong(KEY_INIT_CHANNEL_ID, channelIdFromIntent);
} 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().
String inputId = mInitChannelUri.getQueryParameter("input");
long channelId = Utils.getLastWatchedChannelIdForInput(this, inputId);
if (channelId == Channel.INVALID_ID) {
String[] projection = {BaseColumns._ID};
long time = System.currentTimeMillis();
try (Cursor cursor =
getContentResolver().query(uri, projection, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
channelId = cursor.getLong(0);
}
}
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log(
"MainActivity queries DB for "
+ "last channel check ("
+ (System.currentTimeMillis() - time)
+ "ms)");
}
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 class AsyncQueryProgramTask extends AsyncDbTask.AsyncQueryTask<Program> {
private final long mChannelIdFromIntent;
public AsyncQueryProgramTask(
Executor executor,
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String orderBy,
long channelId) {
super(executor, MainActivity.this, uri, projection, selection, selectionArgs, orderBy);
mChannelIdFromIntent = channelId;
}
@Override
protected Program onQuery(Cursor c) {
Program program = null;
if (c != null && c.moveToNext()) {
program = ProgramImpl.fromCursor(c);
}
return program;
}
@Override
protected void onPostExecute(Program program) {
if (program == null || program.getStartTimeUtcMillis() <= System.currentTimeMillis()) {
// null or current program
return;
}
Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent);
if (channel != null) {
Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
intent.putExtra(DetailsActivity.CHANNEL_ID, mChannelIdFromIntent);
intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, DetailsActivity.PROGRAM_VIEW);
intent.putExtra(DetailsActivity.PROGRAM, program.toParcelable());
intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId());
startActivity(intent);
}
}
}
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);
}
mAudioManagerHelper.abandonAudioFocus();
mMediaSessionWrapper.setPlaybackState(false);
}
TvSingletons.getSingletons(this)
.getMainActivityWrapper()
.notifyCurrentChannelChange(this, null);
mChannelTuner.resetCurrentChannel();
mTunePending = false;
}
private void scheduleRestoreMainTvView() {
mHandler.removeCallbacks(mRestoreMainViewRunnable);
mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS);
}
/** Says {@code text} when accessibility is turned on. */
private 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 tune(boolean updateChannelBanner) {
if (DEBUG) Log.d(TAG, "tune()");
mTuneDurationTimer.start();
lazyInitializeIfNeeded();
// Prerequisites to be able to tune.
if (mInputIdUnderSetup != null) {
mTunePending = true;
return;
}
mTunePending = false;
if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(this)) {
mTvView.resetChannelSignalStrength();
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
}
final Channel channel = mChannelTuner.getCurrentChannel();
SoftPreconditions.checkState(channel != null);
if (channel == null) {
return;
}
if (!mChannelTuner.isCurrentChannelPassthrough()) {
if (mTvInputManagerHelper.getTunerTvInputSize() == 0) {
Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show();
finish();
return;
}
if (mSetupUtils.isFirstTune()) {
if (!mChannelTuner.areAllChannelsLoaded()) {
// tune() will be called, once all channels are loaded.
stopTv("tune()", false);
return;
}
if (mChannelDataManager.getChannelCount() > 0) {
mOverlayManager.showIntroDialog();
} else {
startOnboardingActivity();
return;
}
}
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 {
mOverlayManager.showSetupFragment();
}
return;
}
if (!CommonUtils.isRunningInTest()
&& mShowNewSourcesFragment
&& mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
// Show new channel sources fragment.
runAfterAttachedToWindow(
() ->
mOverlayManager.runAfterOverlaysAreClosed(
new Runnable() {
@Override
public void run() {
mOverlayManager.showNewSourcesFragment();
}
}));
}
mSetupUtils.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;
}
// For every tune, we need to inform the tuned channel or input to a user,
// if Talkback is turned on.
sendAccessibilityText(
mChannelTuner.isCurrentChannelPassthrough()
? Utils.loadLabel(
this, mTvInputManagerHelper.getTvInputInfo(channel.getInputId()))
: channel.getDisplayText());
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);
TvSingletons.getSingletons(this)
.getMainActivityWrapper()
.notifyCurrentChannelChange(this, channel);
}
// We have to provide channel here instead of using TvView's channel, because TvView's
// channel might be null when there's tuner conflict. In that case, TvView will resets
// its current channel onConnectionFailed().
checkChannelLockNeeded(mTvView, channel);
if (updateChannelBanner) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.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);
}
mMediaSessionWrapper.update(mTvView.isBlocked(), getCurrentChannel(), getCurrentProgram());
}
// Runs the runnable after the activity is attached to window to show the fragment transition
// animation.
// The runnable runs asynchronously to show the animation a little better even when system is
// busy at the moment it is called.
// If the activity is paused shortly, runnable may not be called because all the fragments
// should be closed when the activity is paused.
private void runAfterAttachedToWindow(final Runnable runnable) {
final Runnable runOnlyIfActivityIsResumed =
() -> {
if (mActivityResumed) {
runnable.run();
}
};
if (mContentView.isAttachedToWindow()) {
mHandler.post(runOnlyIfActivityIsResumed);
} else {
mContentView
.getViewTreeObserver()
.addOnWindowAttachListener(
new ViewTreeObserver.OnWindowAttachListener() {
@Override
public void onWindowAttached() {
mContentView
.getViewTreeObserver()
.removeOnWindowAttachListener(this);
mHandler.post(runOnlyIfActivityIsResumed);
}
@Override
public void onWindowDetached() {}
});
}
}
boolean isNowPlayingProgram(Channel channel, Program program) {
return program == null
? (channel != null
&& getCurrentProgram() == null
&& channel.equals(getCurrentChannel()))
: program.equals(getCurrentProgram());
}
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 currentChannel) {
if (currentChannel == null) {
currentChannel = tvView.getCurrentChannel();
}
if (tvView.isPlaying() && currentChannel != null) {
if (getParentalControlSettings().isParentalControlsEnabled()
&& currentChannel.isLocked()
&& !mShowLockedChannelsTemporarily
&& !(isUnderShrunkenTvView()
&& currentChannel.equals(mChannelBeforeShrunkenTvView)
&& mWasChannelUnblockedBeforeShrunkenByUser)) {
if (DEBUG) Log.d(TAG, "Channel " + currentChannel.getId() + " is locked");
blockOrUnblockScreen(tvView, true);
} else {
blockOrUnblockScreen(tvView, false);
}
}
}
private void blockOrUnblockScreen(TunableTvView tvView, boolean blockOrUnblock) {
tvView.blockOrUnblockScreen(blockOrUnblock);
if (tvView == mTvView) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
mMediaSessionWrapper.update(blockOrUnblock, getCurrentChannel(), getCurrentProgram());
}
}
/** 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);
}
@VisibleForTesting
protected void applyMultiAudio(String trackId) {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
if (tracks == null) {
mTvOptionsManager.onMultiAudioChanged(null);
return;
}
TvTrackInfo bestTrack = null;
if (trackId != null) {
for (TvTrackInfo track : tracks) {
if (trackId.equals(track.getId())) {
bestTrack = track;
break;
}
}
}
if (bestTrack == null) {
String id = TvSettings.getMultiAudioId(this);
String language = TvSettings.getMultiAudioLanguage(this);
int channelCount = TvSettings.getMultiAudioChannelCount(this);
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, UNDEFINED_TRACK_INDEX);
} else {
mTvOptionsManager.onMultiAudioChanged(
TvTrackInfoUtils.getMultiAudioString(this, bestTrack, false));
}
return;
}
mTvOptionsManager.onMultiAudioChanged(null);
}
private void applyClosedCaption() {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks == null) {
mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
return;
}
boolean enabled = mCaptionSettings.isEnabled();
mTvView.setClosedCaptionEnabled(enabled);
String selectedTrackId = getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE);
if (enabled) {
String language = mCaptionSettings.getLanguage();
String trackId = mCaptionSettings.getTrackId();
List<String> preferredLanguages = mCaptionSettings.getSystemPreferenceLanguageList();
int bestTrackIndex =
findBestCaptionTrackIndex(tracks, language, preferredLanguages, trackId);
if (bestTrackIndex != UNDEFINED_TRACK_INDEX) {
selectCaptionTrack(selectedTrackId, tracks.get(bestTrackIndex), bestTrackIndex);
return;
}
}
deselectCaptionTrack(selectedTrackId);
}
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()");
Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
SideFragment.releaseRecycledViewPool();
ViewCache.getInstance().clear();
if (mTvView != null) {
mTvView.release();
}
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 (mOverlayManager != null) {
mAccessibilityManager.removeAccessibilityStateChangeListener(mOverlayManager);
mOverlayManager.release();
}
mMemoryManageables.clear();
if (mMediaSessionWrapper != null) {
mMediaSessionWrapper.release();
}
if (mAudioCapabilitiesReceiver != null) {
mAudioCapabilitiesReceiver.unregister();
}
mHandler.removeCallbacksAndMessages(null);
application.getMainActivityWrapper().onMainActivityDestroyed(this);
if (mTvInputManagerHelper != null) {
mTvInputManagerHelper.clearTvInputLabels();
if (mOptionalBuiltInTunerManager.isPresent()) {
mTvInputManagerHelper.removeCallback(mTvInputCallback);
}
}
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
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:
// fall 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) {
channelUpPressed();
}
return true;
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
channelDownPressed();
}
return true;
default: // fall out
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public void channelDown() {
channelDownPressed();
finishChannelChangeIfNeeded();
}
private void channelDownPressed() {
// message sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
moveToAdjacentChannel(false, false);
mTracker.sendChannelDown();
}
@Override
public void channelUp() {
channelUpPressed();
finishChannelChangeIfNeeded();
}
private void channelUpPressed() {
// message sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
moveToAdjacentChannel(true, false);
mTracker.sendChannelUp();
}
@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
* G debug: refresh cloud epg
* I KEYCODE_TV_INPUT
* O debug: show display mode option
* S KEYCODE_CAPTIONS: select subtitle
* W debug: toggle screen size
* V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
*/
if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
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) {
// Prevent MainActivity from being closed by onVisibleBehindCanceled()
mOtherActivityLaunched = true;
return false;
}
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:
// fall 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;
default: // fall out
}
} else {
if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
mOverlayManager.showKeypadChannelSwitch(keyCode);
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (!mTvView.isVideoOrAudioAvailable()
&& mTvView.getVideoUnavailableReason()
== TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) {
DvrUiHelper.startSchedulesActivityForTuneConflict(
this, mChannelTuner.getCurrentChannel());
return true;
}
showPinDialogFragment();
return true;
case KeyEvent.KEYCODE_WINDOW:
enterPictureInPictureMode();
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 blocklisted will have FLAG_CANCELED.
// See dispatchKeyEvent() for detail.
return true;
}
if (keyCode != KeyEvent.KEYCODE_MENU) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
}
if (keyCode != KeyEvent.KEYCODE_E) {
mOverlayManager.showMenu(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 (!DeveloperPreferences.USE_DEBUG_KEYS.get(this)) {
break;
}
// fall through.
case KeyEvent.KEYCODE_CAPTIONS:
mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
return true;
case KeyEvent.KEYCODE_A:
if (!DeveloperPreferences.USE_DEBUG_KEYS.get(this)) {
break;
}
// fall through.
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment());
return true;
case KeyEvent.KEYCODE_INFO:
mOverlayManager.showBanner();
return true;
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_V:
Channel currentChannel = getCurrentChannel();
if (currentChannel != null && mDvrManager != null) {
boolean isRecording =
mDvrManager.getCurrentRecording(currentChannel.getId()) != null;
if (!isRecording) {
if (!mDvrManager.isChannelRecordable(currentChannel)) {
Toast.makeText(
this,
R.string.dvr_msg_cannot_record_program,
Toast.LENGTH_SHORT)
.show();
} else {
Program program =
mProgramDataManager.getCurrentProgram(
currentChannel.getId());
DvrUiHelper.checkStorageStatusAndShowErrorMessage(
this,
currentChannel.getInputId(),
() ->
DvrUiHelper.requestRecordingCurrentProgram(
MainActivity.this,
currentChannel,
program,
false));
}
} else {
DvrUiHelper.showStopRecordingDialog(
this,
currentChannel.getId(),
DvrStopRecordingFragment.REASON_USER_STOP,
new HalfSizedDialogFragment.OnActionClickListener() {
@Override
public void onActionClick(long actionId) {
if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
ScheduledRecording currentRecording =
mDvrManager.getCurrentRecording(
currentChannel.getId());
if (currentRecording != null) {
mDvrManager.stopRecording(currentRecording);
}
}
}
});
}
}
return true;
default: // fall out
}
}
if (keyCode == KeyEvent.KEYCODE_WINDOW) {
// Consumes the PIP button to prevent entering PIP mode
// in case that TV isn't showing properly (e.g. no browsable channel)
return true;
}
if (DeveloperPreferences.USE_DEBUG_KEYS.get(this) || BuildConfig.ENG) {
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.setTvViewLayoutParams(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.setTvViewLayoutParams(params);
}
return true;
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT:
mUseKeycodeBlocklist = !mUseKeycodeBlocklist;
return true;
case KeyEvent.KEYCODE_O:
mOverlayManager.getSideFragmentManager().show(new DisplayModeFragment());
return true;
case KeyEvent.KEYCODE_D:
mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment());
return true;
default: // fall out
}
}
return super.onKeyUp(keyCode, event);
}
private void showPinDialogFragment() {
if (!PermissionUtils.hasModifyParentalControls(this)) {
return;
}
PinDialogFragment dialog = null;
if (mTvView.isScreenBlocked()) {
dialog = PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
} else if (mTvView.isContentBlocked()) {
dialog =
PinDialogFragment.create(
PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
mTvView.getBlockedContentRating().flattenToString());
}
if (dialog != null) {
mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, false);
}
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (DeveloperPreferences.LOG_KEYEVENT.get(this)) 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 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.
mIsInPIPMode = true;
if (mOverlayManager.isOverlayOpened()) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
mHandler.post(MainActivity.super::enterPictureInPictureMode);
} else {
MainActivity.super.enterPictureInPictureMode();
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (!hasFocus) {
finishChannelChangeIfNeeded();
}
}
/**
* Returns {@code true} if one of the channel changing keys are pressed and not released yet.
*/
public 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 (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
}
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 a passthrough channel, we don't handle the key events in TV
// activity. Instead, the key event will be handled by the passthrough TV input.
return mChannelTuner.isCurrentChannelPassthrough();
}
private void tuneToLastWatchedChannelForTunerInput() {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
return;
}
stopTv();
startTv(null);
}
public void tuneToChannel(Channel channel) {
if (channel == null) {
if (mTvView.isPlaying()) {
mTvView.reset();
}
} else {
if (!mTvView.isPlaying()) {
startTv(channel.getUri());
} else if (channel.equals(mTvView.getCurrentChannel())) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
} else if (channel.equals(mChannelTuner.getCurrentChannel())) {
// Channel banner is already updated in moveToAdjacentChannel
tune(false);
} else if (mChannelTuner.moveToChannel(channel)) {
// Channel banner would be updated inside of tune.
tune(true);
} 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)) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
fastTuning
? TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST
: TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
}
/** Set the main TV view which holds HDMI-CEC active source based on the sound mode */
private void restoreMainTvView() {
mTvView.setMain();
}
@Override
public void onVisibleBehindCanceled() {
stopTv("onVisibleBehindCanceled()", false);
mTracker.sendScreenView("");
mAudioManagerHelper.abandonAudioFocus();
mMediaSessionWrapper.setPlaybackState(false);
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 startActivityForResult(Intent intent, int requestCode) {
mOtherActivityLaunched = true;
if (intent.getCategories() == null
|| !intent.getCategories().contains(Intent.CATEGORY_HOME)) {
// Workaround b/30150267
requestVisibleBehind(false);
}
super.startActivityForResult(intent, requestCode);
}
public List<TvTrackInfo> getTracks(int type) {
return mTvView.getTracks(type);
}
public String getSelectedTrack(int type) {
return mTvView.getSelectedTrack(type);
}
@VisibleForTesting
static int findBestCaptionTrackIndex(
List<TvTrackInfo> tracks,
String selectedLanguage,
List<String> preferredLanguages,
String selectedTrackId) {
int alternativeTrackIndex = UNDEFINED_TRACK_INDEX;
// Priority of selected alternative track, where -1 being the highest priority.
int alternativeTrackPriority = preferredLanguages.size();
for (int i = 0; i < tracks.size(); i++) {
TvTrackInfo track = tracks.get(i);
if (Utils.isEqualLanguage(track.getLanguage(), selectedLanguage)) {
if (track.getId().equals(selectedTrackId)) {
return i;
} else if (alternativeTrackPriority != HIGHEST_PRIORITY) {
alternativeTrackIndex = i;
alternativeTrackPriority = HIGHEST_PRIORITY;
}
} else {
// Select alternative track in order of preference
// 1. User language captions
// 2. System language captions
// 3. Other captions
int index = UNDEFINED_TRACK_INDEX;
for (int j = 0; j < preferredLanguages.size(); j++) {
if (Utils.isEqualLanguage(track.getLanguage(), preferredLanguages.get(j))) {
index = j;
break;
}
}
if (index != UNDEFINED_TRACK_INDEX && index < alternativeTrackPriority) {
alternativeTrackIndex = i;
alternativeTrackPriority = index;
} else if (alternativeTrackIndex == UNDEFINED_TRACK_INDEX) {
alternativeTrackIndex = i;
}
}
}
return alternativeTrackIndex;
}
private void selectTrack(int type, TvTrackInfo track, int trackIndex) {
mTvView.selectTrack(type, track == null ? null : track.getId());
if (type == TvTrackInfo.TYPE_AUDIO) {
mTvOptionsManager.onMultiAudioChanged(
track == null
? null
: TvTrackInfoUtils.getMultiAudioString(this, track, false));
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
}
}
private void selectCaptionTrack(String selectedTrackId, TvTrackInfo track, int trackIndex) {
if (!track.getId().equals(selectedTrackId)) {
selectTrack(TvTrackInfo.TYPE_SUBTITLE, track, trackIndex);
} else {
// Already selected. Update the option string only.
mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
}
if (DEBUG) {
Log.d(
TAG,
"Subtitle Track Selected {id="
+ track.getId()
+ ", language="
+ track.getLanguage()
+ "}");
}
}
private void deselectCaptionTrack(String selectedTrackId) {
if (selectedTrackId != null) {
selectTrack(TvTrackInfo.TYPE_SUBTITLE, null, UNDEFINED_TRACK_INDEX);
if (DEBUG) Log.d(TAG, "Subtitle Track Unselected");
} else {
mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
}
}
public void selectAudioTrack(String trackId) {
saveMultiAudioSetting(trackId);
applyMultiAudio(trackId);
}
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() {
if (mTvView.isVideoAvailable()
|| !Objects.equals(
mTvView.getCurrentChannel(), mChannelTuner.getCurrentChannel())) {
return;
}
switch (mTvView.getVideoUnavailableReason()) {
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
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 CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
Toast.makeText(
this,
R.string.msg_channel_unavailable_not_connected,
Toast.LENGTH_SHORT)
.show();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT)
.show();
break;
}
}
/** Returns {@code true} if some overlay UI will be shown when the activity is resumed. */
public boolean willShowOverlayUiWhenResume() {
return mInputToSetUp != null || mShowProgramGuide || mShowSelectInputView;
}
/** Returns the current parental control settings. */
public ParentalControlSettings getParentalControlSettings() {
return mTvInputManagerHelper.getParentalControlSettings();
}
/** Returns a ContentRatingsManager instance. */
public ContentRatingsManager getContentRatingsManager() {
return mTvInputManagerHelper.getContentRatingsManager();
}
/** Returns the current captioning settings. */
public CaptionSettings getCaptionSettings() {
return mCaptionSettings;
}
/** Adds the {@link OnActionClickListener}. */
public void addOnActionClickListener(OnActionClickListener listener) {
mOnActionClickListeners.add(listener);
}
/** Removes the {@link OnActionClickListener}. */
public void removeOnActionClickListener(OnActionClickListener listener) {
mOnActionClickListeners.remove(listener);
}
@Override
public boolean onActionClick(String category, int actionId, Bundle params) {
// There should be only one action listener per an action.
for (OnActionClickListener l : mOnActionClickListeners) {
if (l.onActionClick(category, actionId, params)) {
return true;
}
}
return false;
}
// 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 (!CommonUtils.isRunningInTest()) {
return;
}
// Only try to set the channels browseable if we are a system app.
if (SYSTEM_APP_FEATURE.isEnabled(getApplicationContext())) {
Utils.enableAllChannels(this);
}
}
// Lazy initialization
private void lazyInitializeIfNeeded() {
// Already initialized.
if (mLazyInitialized) {
return;
}
mLazyInitialized = true;
// Running initialization.
mHandler.postDelayed(
() -> {
if (mActivityStarted) {
initAnimations();
initSideFragments();
initMenuItemViews();
}
},
LAZY_INITIALIZATION_DELAY);
}
private void initAnimations() {
mTvViewUiManager.initAnimatorIfNeeded();
mOverlayManager.initAnimatorIfNeeded();
}
private void initSideFragments() {
SideFragment.preloadItemViews(this);
}
private void initMenuItemViews() {
mOverlayManager.getMenu().preloadItemViews();
}
private boolean isAudioOnlyInput() {
if (mLastInputIdFromIntent == null) {
return false;
}
TvInputInfoCompat inputInfo =
mTvInputManagerHelper.getTvInputInfoCompat(mLastInputIdFromIntent);
return inputInfo != null && inputInfo.isAudioOnly();
}
@Nullable
private String getInputId(Intent intent) {
Uri uri = intent.getData();
return TvContract.isChannelUriForPassthroughInput(uri)
? uri.getPathSegments().get(1)
: null;
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
for (MemoryManageable memoryManageable : mMemoryManageables) {
memoryManageable.performTrimMemory(level);
}
}
@Override
public AndroidInjector<Object> androidInjector() {
return mAndroidInjector;
}
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;
// message re-sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
mainActivity.moveToAdjacentChannel(false, true);
break;
case MSG_CHANNEL_UP_PRESSED:
startTime = (Long) msg.obj;
// message re-sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
mainActivity.moveToAdjacentChannel(true, true);
break;
default: // fall out
}
}
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;
}
}
/** {@link OnTuneListener} implementation */
@VisibleForTesting
protected class MyOnTuneListener implements OnTuneListener {
boolean mUnlockAllowedRatingBeforeShrunken = true;
boolean mWasUnderShrunkenTvView;
Channel mChannel;
private void onTune(Channel channel, boolean wasUnderShrunkenTvView) {
Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
mChannel = channel;
mWasUnderShrunkenTvView = wasUnderShrunkenTvView;
// Fetch complete projection of tuned channel.
mProgramDataManager.onChannelTuned(channel.getId());
}
@Override
public void onUnexpectedStop(Channel channel) {
stopTv();
startTv(null);
}
@Override
public void onTuneFailed(Channel channel) {
Log.w(TAG, "onTuneFailed(" + channel + ")");
if (mTvView.isFadedOut()) {
mTvView.removeFadeEffect();
}
Toast.makeText(
MainActivity.this,
R.string.msg_channel_unavailable_unknown,
Toast.LENGTH_SHORT)
.show();
}
@Override
public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {
if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset());
}
if (info.isVideoOrAudioAvailable() && mChannel.equals(getCurrentChannel())) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO);
}
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvAspectRatio();
applyMultiAudio(
allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
applyClosedCaption();
mOverlayManager.getMenu().onStreamInfoChanged();
if (mTvView.isVideoAvailable()) {
mTvViewUiManager.fadeInTvView();
}
if (!mTvView.isContentBlocked() && !mTvView.isScreenBlocked()) {
updateAvailabilityToast();
}
mHandler.removeCallbacks(mRestoreMainViewRunnable);
restoreMainTvView();
}
@Override
public void onChannelRetuned(Uri channel) {
if (channel == null) {
return;
}
Channel currentChannel =
mChannelDataManager.getChannel(ContentUriUtils.safeParseId(channel));
if (currentChannel == null) {
Log.e(
TAG,
"onChannelRetuned is called but can't find a channel with the URI "
+ channel);
return;
}
/* Begin_AOSP_Comment_Out
if (PLUTO_TV_PACKAGE_NAME.equals(currentChannel.getPackageName())) {
// Do nothing for the Pluto TV input because it misuses this API. b/22720711.
return;
}
End_AOSP_Comment_Out */
if (isChannelChangeKeyDownReceived()) {
// Ignore this message if the user is changing the channel.
return;
}
mChannelTuner.setCurrentChannel(currentChannel);
mTvView.setCurrentChannel(currentChannel);
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
@Override
public void onContentBlocked() {
Debug.getTimer(Debug.TAG_START_UP_TIMER)
.log("MainActivity.MyOnTuneListener.onContentBlocked removes timer");
Debug.removeTimer(Debug.TAG_START_UP_TIMER);
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
&& Objects.equals(mChannelBeforeShrunkenTvView, mChannel)
&& rating.equals(mAllowedRatingBeforeShrunken)) {
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
}
mOverlayManager.setBlockingContentRating(rating);
mTvViewUiManager.fadeInTvView();
mMediaSessionWrapper.update(true, getCurrentChannel(), getCurrentProgram());
}
@Override
public void onContentAllowed() {
if (!isUnderShrunkenTvView()) {
mUnlockAllowedRatingBeforeShrunken = false;
}
mOverlayManager.setBlockingContentRating(null);
mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram());
}
@Override
public void onChannelSignalStrength() {
if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(getApplicationContext())) {
mOverlayManager.updateChannelBannerAndShowIfNeeded(
TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
}
}
}
private class MySingletonsImpl implements MySingletons {
@Override
public Provider<Channel> getCurrentChannelProvider() {
return MainActivity.this::getCurrentChannel;
}
@Override
public Provider<Program> getCurrentProgramProvider() {
return MainActivity.this::getCurrentProgram;
}
@Override
public Provider<TvOverlayManager> getOverlayManagerProvider() {
return MainActivity.this::getOverlayManager;
}
@Override
public TvInputManagerHelper getTvInputManagerHelperSingleton() {
return getTvInputManagerHelper();
}
@Override
public Provider<Long> getCurrentPlayingPositionProvider() {
return MainActivity.this::getCurrentPlayingPosition;
}
@Override
public DvrManager getDvrManagerSingleton() {
return TvSingletons.getSingletons(getApplicationContext()).getDvrManager();
}
}
/** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */
@dagger.Module
public abstract static class Module {
@ContributesAndroidInjector
abstract MainActivity contributesMainActivityActivityInjector();
@ContributesAndroidInjector
abstract DeveloperOptionFragment contributesDeveloperOptionFragment();
@ContributesAndroidInjector
abstract RatingsFragment contributesRatingsFragment();
@ContributesAndroidInjector
abstract ProgramItemView contributesProgramItemView();
@ContributesAndroidInjector
abstract DvrAlreadyRecordedFragment contributesDvrAlreadyRecordedFragment();
@ContributesAndroidInjector
abstract DvrAlreadyScheduledFragment contributesDvrAlreadyScheduledFragment();
@ContributesAndroidInjector
abstract DvrScheduleFragment contributesDvrScheduleFragment();
}
}