/* * 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.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, 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 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 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 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 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 mRecentChannels = new ArrayDeque<>(MAX_RECENT_CHANNELS); private String mLastInputIdFromIntent; private final Handler mHandler = new MainActivityHandler(this); private final Set 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) {} }; 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 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. * *

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. * *

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 { 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 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 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 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 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 getTracks(int type) { return mTvView.getTracks(type); } public String getSelectedTrack(int type) { return mTvView.getSelectedTrack(type); } @VisibleForTesting static int findBestCaptionTrackIndex( List tracks, String selectedLanguage, List 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 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 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 androidInjector() { return mAndroidInjector; } private static class MainActivityHandler extends WeakHandler { 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 getCurrentChannelProvider() { return MainActivity.this::getCurrentChannel; } @Override public Provider getCurrentProgramProvider() { return MainActivity.this::getCurrentProgram; } @Override public Provider getOverlayManagerProvider() { return MainActivity.this::getOverlayManager; } @Override public TvInputManagerHelper getTvInputManagerHelperSingleton() { return getTvInputManagerHelper(); } @Override public Provider 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(); } }