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