TvActivity.java revision 545b3ba127213dbe4e388cf99dc7f97b66a46694
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv;
18
19import android.app.Activity;
20import android.app.DialogFragment;
21import android.app.Fragment;
22import android.app.FragmentManager;
23import android.app.FragmentTransaction;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.graphics.Point;
29import android.media.AudioManager;
30import android.media.tv.TvInputInfo;
31import android.media.tv.TvInputManager;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Message;
36import android.preference.PreferenceManager;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.Display;
40import android.view.GestureDetector;
41import android.view.GestureDetector.SimpleOnGestureListener;
42import android.view.Gravity;
43import android.view.InputEvent;
44import android.view.KeyEvent;
45import android.view.MotionEvent;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.animation.Animation;
49import android.view.animation.AnimationUtils;
50import android.widget.FrameLayout;
51import android.widget.LinearLayout;
52import android.widget.Toast;
53
54import com.android.tv.data.DisplayMode;
55import com.android.tv.data.Channel;
56import com.android.tv.data.ChannelMap;
57import com.android.tv.data.StreamInfo;
58import com.android.tv.dialog.EditInputDialogFragment;
59import com.android.tv.dialog.RecentlyWatchedDialogFragment;
60import com.android.tv.input.TisTvInput;
61import com.android.tv.input.TvInput;
62import com.android.tv.input.UnifiedTvInput;
63import com.android.tv.ui.DisplayModeOptionFragment;
64import com.android.tv.ui.BaseSideFragment;
65import com.android.tv.ui.ChannelBannerView;
66import com.android.tv.ui.ClosedCaptionOptionFragment;
67import com.android.tv.ui.EditChannelsFragment;
68import com.android.tv.ui.InputPickerFragment;
69import com.android.tv.ui.MainMenuView;
70import com.android.tv.ui.ChannelNumberView;
71import com.android.tv.ui.PipLocationFragment;
72import com.android.tv.ui.SidePanelContainer;
73import com.android.tv.ui.SimpleGuideFragment;
74import com.android.tv.ui.TunableTvView;
75import com.android.tv.ui.TunableTvView.OnTuneListener;
76import com.android.tv.util.TvInputManagerHelper;
77import com.android.tv.util.TvSettings;
78import com.android.tv.util.Utils;
79
80import java.util.HashSet;
81
82/**
83 * The main activity for demonstrating TV app.
84 */
85public class TvActivity extends Activity implements AudioManager.OnAudioFocusChangeListener {
86    // STOPSHIP: Turn debugging off
87    private static final boolean DEBUG = true;
88    private static final boolean USE_DEBUG_KEYS = true;
89    private static final String TAG = "TvActivity";
90
91    private static final int MSG_START_TV_RETRY = 1;
92
93    private static final int DURATION_SHOW_CHANNEL_BANNER = 8000;
94    private static final int DURATION_SHOW_CONTROL_GUIDE = 1000;
95    private static final int DURATION_SHOW_MAIN_MENU = 5000;
96    private static final int DURATION_SHOW_SIDE_FRAGMENT = 60000;
97    private static final float AUDIO_MAX_VOLUME = 1.0f;
98    private static final float AUDIO_MIN_VOLUME = 0.0f;
99    private static final float AUDIO_DUCKING_VOLUME = 0.3f;
100    // Wait for 3 seconds
101    private static final int START_TV_MAX_RETRY = 12;
102    private static final int START_TV_RETRY_INTERVAL = 250;
103
104    private static final int SIDE_FRAGMENT_TAG_SHOW = 0;
105    private static final int SIDE_FRAGMENT_TAG_HIDE = 1;
106    private static final int SIDE_FRAGMENT_TAG_RESET = 2;
107
108    private static final int[] KEYCODE_BLACKLIST = {
109        KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN,
110    };
111
112    private static final int REQUEST_START_SETUP_ACTIIVTY = 0;
113    private static final int REQUEST_START_SETTINGS_ACTIIVTY = 1;
114
115    private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>();
116
117    private TunableTvView mTvView;
118    private LinearLayout mControlGuide;
119    private MainMenuView mMainMenuView;
120    private ChannelBannerView mChannelBanner;
121    private ChannelNumberView mChannelNumberView;
122    private SidePanelContainer mSidePanelContainer;
123    private HideRunnable mHideChannelBanner;
124    private HideRunnable mHideControlGuide;
125    private HideRunnable mHideMainMenu;
126    private HideRunnable mHideSideFragment;
127    private int mShortAnimationDuration;
128    private int mDisplayWidth;
129    private GestureDetector mGestureDetector;
130    private ChannelMap mChannelMap;
131    private long mInitChannelId;
132    private String mInitTvInputId;
133    private boolean mChildActivityCanceled;
134
135    private TvInput mTvInputForSetup;
136    private TvInputManagerHelper mTvInputManagerHelper;
137    private AudioManager mAudioManager;
138    private int mAudioFocusStatus;
139    private boolean mTunePendding;
140    private boolean mPipEnabled;
141    private long mPipChannelId;
142    private boolean mDebugNonFullSizeScreen;
143    private boolean mActivityResumed;
144    private boolean mSilenceRequired;
145    private boolean mUseKeycodeBlacklist;
146    private boolean mIsShy = true;
147
148    private boolean mIsClosedCaptionEnabled;
149    private int mDisplayMode;
150    private int mPipLocation;
151    private SharedPreferences mSharedPreferences;
152
153    private SimpleGuideFragment mSimpleGuideFragment;
154
155    static {
156        AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
157        AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG);
158    }
159
160    // PIP is used for debug/verification of multiple sessions rather than real PIP feature.
161    // When PIP is enabled, the same channel as mTvView is tuned.
162    private TunableTvView mPipView;
163
164    private final Handler mHandler = new Handler() {
165        @Override
166        public void handleMessage(Message msg) {
167            if (msg.what == MSG_START_TV_RETRY) {
168                Object[] arg = (Object[]) msg.obj;
169                TvInput input = (TvInput) arg[0];
170                long channelId = (Long) arg[1];
171                int retryCount = msg.arg1;
172                startTvIfAvailableOrRetry(input, channelId, retryCount);
173            }
174        }
175    };
176
177    @Override
178    protected void onCreate(Bundle savedInstanceState) {
179        super.onCreate(savedInstanceState);
180
181        setContentView(R.layout.activity_tv);
182        mTvView = (TunableTvView) findViewById(R.id.tv_view);
183        mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() {
184            @Override
185            public boolean onUnhandledInputEvent(InputEvent event) {
186                if (event instanceof KeyEvent) {
187                    KeyEvent keyEvent = (KeyEvent) event;
188                    if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.isLongPress()) {
189                        if (onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) {
190                            return true;
191                        }
192                    }
193                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
194                        return onKeyUp(keyEvent.getKeyCode(), keyEvent);
195                    } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
196                        return onKeyDown(keyEvent.getKeyCode(), keyEvent);
197                    }
198                } else if (event instanceof MotionEvent) {
199                    MotionEvent motionEvent = (MotionEvent) event;
200                    if (motionEvent.isTouchEvent()) {
201                        return onTouchEvent(motionEvent);
202                    }
203                }
204                return false;
205            }
206        });
207        mPipView = (TunableTvView) findViewById(R.id.pip_view);
208        mPipView.setPip(true);
209
210        mControlGuide = (LinearLayout) findViewById(R.id.control_guide);
211        mChannelBanner = (ChannelBannerView) findViewById(R.id.channel_banner);
212        mMainMenuView = (MainMenuView) findViewById(R.id.main_menu);
213        mSidePanelContainer = (SidePanelContainer) findViewById(R.id.right_panel);
214        mMainMenuView.setTvActivity(this);
215        mChannelNumberView = (ChannelNumberView) findViewById(R.id.channel_number_view);
216        mChannelNumberView.setTvActivity(this);
217
218        // Initially hide the channel banner and the control guide.
219        mChannelBanner.setVisibility(View.GONE);
220        mMainMenuView.setVisibility(View.GONE);
221        mControlGuide.setVisibility(View.GONE);
222        mSidePanelContainer.setVisibility(View.GONE);
223        mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET);
224
225        mHideControlGuide = new HideRunnable(mControlGuide, DURATION_SHOW_CONTROL_GUIDE);
226        mHideChannelBanner = new HideRunnable(mChannelBanner, DURATION_SHOW_CHANNEL_BANNER);
227        mHideMainMenu = new HideRunnable(mMainMenuView, DURATION_SHOW_MAIN_MENU,
228                new Runnable() {
229                    @Override
230                    public void run() {
231                        if (mPipEnabled) {
232                            mPipView.setVisibility(View.INVISIBLE);
233                        }
234                    }
235                },
236                new Runnable() {
237                    @Override
238                    public void run() {
239                        if (mPipEnabled && mActivityResumed) {
240                            mPipView.setVisibility(View.VISIBLE);
241                        }
242                    }
243                });
244        mHideSideFragment = new HideRunnable(mSidePanelContainer, DURATION_SHOW_SIDE_FRAGMENT, null,
245                new Runnable() {
246                    @Override
247                    public void run() {
248                        resetSideFragment();
249                    }
250                });
251
252        mShortAnimationDuration = getResources().getInteger(
253                android.R.integer.config_shortAnimTime);
254
255        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
256        mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
257        Display display = getWindowManager().getDefaultDisplay();
258        Point size = new Point();
259        display.getSize(size);
260        mDisplayWidth = size.x;
261
262        mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() {
263            static final float CONTROL_MARGIN = 0.2f;
264            final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN;
265            final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN);
266
267            @Override
268            public boolean onDown(MotionEvent event) {
269                if (DEBUG) Log.d(TAG, "onDown: " + event.toString());
270                if (mChannelMap == null) {
271                    return false;
272                }
273
274                mHideControlGuide.showAndHide();
275
276                if (event.getX() <= mLeftMargin) {
277                    channelDown();
278                    return true;
279                } else if (event.getX() >= mRightMargin) {
280                    channelUp();
281                    return true;
282                }
283                return false;
284            }
285
286            @Override
287            public boolean onSingleTapUp(MotionEvent event) {
288                if (mChannelMap == null) {
289                    showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
290                    return true;
291                }
292
293                if (event.getX() > mLeftMargin && event.getX() < mRightMargin) {
294                    displayMainMenu(true);
295                    return true;
296                }
297                return false;
298            }
299        });
300
301        TvInputManager manager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
302        mTvInputManagerHelper = new TvInputManagerHelper(manager);
303        mTvInputManagerHelper.start();
304
305        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
306        restoreClosedCaptionEnabled();
307        restoreDisplayMode();
308        restorePipLocation();
309        onNewIntent(getIntent());
310    }
311
312    @Override
313    protected void onNewIntent(Intent intent) {
314        // Handle the passed key press, if any. Note that only the key codes that are currently
315        // handled in the TV app will be handled via Intent.
316        // TODO: Consider defining a separate intent filter as passing data of mime type
317        // vnd.android.cursor.item/channel isn't really necessary here.
318        int keyCode = intent.getIntExtra(Utils.EXTRA_KEYCODE, KeyEvent.KEYCODE_UNKNOWN);
319        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
320            if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode);
321            KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
322            onKeyUp(keyCode, event);
323            return;
324        }
325
326        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
327            // In case the channel is given explicitly, use it.
328            mInitChannelId = ContentUris.parseId(intent.getData());
329        } else {
330            mInitChannelId = Channel.INVALID_ID;
331        }
332    }
333
334    @Override
335    protected void onStart() {
336        super.onStart();
337    }
338
339    @Override
340    protected void onResume() {
341        super.onResume();
342        mActivityResumed = true;
343        mSilenceRequired = false;
344        mTvInputManagerHelper.update();
345        if (mTvInputManagerHelper.getTvInputSize() == 0) {
346            Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show();
347            // TODO: Direct the user to a Play Store landing page for TvInputService apps.
348            return;
349        }
350        boolean tvStarted = false;
351        if (mInitTvInputId != null) {
352            TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(mInitTvInputId);
353            if (inputInfo != null) {
354                startTvIfAvailableOrRetry(new TisTvInput(mTvInputManagerHelper, inputInfo, this),
355                        Channel.INVALID_ID, 0);
356                tvStarted = true;
357            }
358        }
359        if (!tvStarted) {
360            startTv(mInitChannelId);
361        }
362        mInitChannelId = Channel.INVALID_ID;
363        mInitTvInputId = null;
364        if (mPipEnabled) {
365            if (!mPipView.isPlaying()) {
366                startPip();
367            } else if (!mPipView.isShown()) {
368                mPipView.setVisibility(View.VISIBLE);
369            }
370        }
371    }
372
373    @Override
374    protected void onPause() {
375        hideOverlays(true, true, true);
376        if (mPipEnabled) {
377            mPipView.setVisibility(View.INVISIBLE);
378        }
379        mActivityResumed = false;
380        super.onPause();
381    }
382
383    private void startTv(long channelId) {
384        if (mTvView.isPlaying()) {
385            // TV has already started.
386            if (channelId == Channel.INVALID_ID) {
387                // Simply adjust the volume without tune.
388                setVolumeByAudioFocusStatus();
389                return;
390            }
391            Uri channelUri = mChannelMap.getCurrentChannelUri();
392            if (channelUri != null && ContentUris.parseId(channelUri) == channelId) {
393                // The requested channel is already tuned.
394                setVolumeByAudioFocusStatus();
395                return;
396            }
397            stopTv();
398        }
399
400        if (channelId == Channel.INVALID_ID) {
401            // If any initial channel id is not given, remember the last channel the user watched.
402            channelId = Utils.getLastWatchedChannelId(this);
403        }
404        if (channelId == Channel.INVALID_ID) {
405            // If failed to pick a channel, try a different input.
406            showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
407            return;
408        }
409        String inputId = Utils.getInputIdForChannel(this, channelId);
410        if (TextUtils.isEmpty(inputId)) {
411            // If the channel is invalid, try to use the last selected physical tv input.
412            inputId = Utils.getLastSelectedPhysInputId(this);
413            if (TextUtils.isEmpty(inputId)) {
414                // If failed to determine the input for that channel, try a different input.
415                showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
416                return;
417            }
418        }
419        TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(inputId);
420        if (inputInfo == null) {
421            // TODO: if the last selected TV input is uninstalled, getLastWatchedChannelId
422            // should return Channel.INVALID_ID.
423            Log.w(TAG, "Input (id=" + inputId + ") doesn't exist");
424            showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
425            return;
426        }
427        String lastSelectedInputId = Utils.getLastSelectedInputId(this);
428        TvInput input;
429        if (UnifiedTvInput.ID.equals(lastSelectedInputId)) {
430            input = new UnifiedTvInput(mTvInputManagerHelper, this);
431        } else {
432            input = new TisTvInput(mTvInputManagerHelper, inputInfo, this);
433        }
434        startTvIfAvailableOrRetry(input, channelId, 0);
435    }
436
437    private void startTvIfAvailableOrRetry(TvInput input, long channelId, int retryCount) {
438        if (!input.isAvailable()) {
439            if (retryCount >= START_TV_MAX_RETRY) {
440                showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
441                return;
442            }
443            if (DEBUG) Log.d(TAG, "Retry start TV (retryCount=" + retryCount + ")");
444            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TV_RETRY,
445                    retryCount + 1, 0, new Object[]{input, channelId}),
446                    START_TV_RETRY_INTERVAL);
447            return;
448        }
449        startTv(input, channelId);
450    }
451
452    @Override
453    protected void onStop() {
454        if (DEBUG) Log.d(TAG, "onStop()");
455        hideOverlays(true, true, true, false);
456        mHandler.removeMessages(MSG_START_TV_RETRY);
457        stopTv();
458        stopPip();
459        super.onStop();
460    }
461
462    public void onInputPicked(TvInput input) {
463        if (input.equals(getSelectedTvInput())) {
464            // Nothing has changed thus nothing to do.
465            return;
466        }
467        if (!mTvInputManagerHelper.isAvailable(input.getId())) {
468            String message = String.format(getString(R.string.input_is_not_available));
469            Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
470            return;
471        }
472        if (!input.hasChannel(false)) {
473            mTvInputForSetup = null;
474            if (!startSetupActivity(input)) {
475                String message = String.format(
476                        getString(R.string.empty_channel_tvinput_and_no_setup_activity),
477                        input.getDisplayName());
478                Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
479                showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
480            }
481            return;
482        }
483
484        stopTv();
485        startTvWithLastWatchedChannel(input);
486    }
487
488    public TvInputManagerHelper getTvInputManagerHelper() {
489        return mTvInputManagerHelper;
490    }
491
492    public TvInput getSelectedTvInput() {
493        return mChannelMap == null ? null : mChannelMap.getTvInput();
494    }
495
496    public void showEditChannelsFragment(int initiator) {
497        showSideFragment(new EditChannelsFragment(mChannelMap.getChannelList(false)), initiator);
498    }
499
500    public boolean startSetupActivity() {
501        if (getSelectedTvInput() == null) {
502            return false;
503        }
504        return startSetupActivity(getSelectedTvInput());
505    }
506
507    public boolean startSetupActivity(TvInput input) {
508        Intent intent = input.getIntentForSetupActivity();
509        if (intent == null) {
510            return false;
511        }
512        startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY);
513        mTvInputForSetup = input;
514        mInitTvInputId = null;
515        stopTv();
516        return true;
517    }
518
519    public boolean startSettingsActivity() {
520        TvInput input = getSelectedTvInput();
521        if (input == null) {
522            Log.w(TAG, "There is no selected TV input during startSettingsActivity");
523            return false;
524        }
525        Intent intent = input.getIntentForSettingsActivity();
526        if (intent == null) {
527            return false;
528        }
529        startActivityForResult(intent, REQUEST_START_SETTINGS_ACTIIVTY);
530        return true;
531    }
532
533    public void showSimpleGuide(int initiator) {
534        mSimpleGuideFragment = new SimpleGuideFragment(this, mChannelMap);
535        showSideFragment(mSimpleGuideFragment, initiator);
536    }
537
538    public void showInputPicker(int initiator) {
539        showSideFragment(new InputPickerFragment(), initiator);
540    }
541
542    public void showDisplayModeOption(int initiator) {
543        showSideFragment(new DisplayModeOptionFragment(), initiator);
544    }
545
546    public void showClosedCaptionOption(int initiator) {
547        showSideFragment(new ClosedCaptionOptionFragment(), initiator);
548    }
549
550    public void showPipLocationOption(int initiator) {
551        showSideFragment(new PipLocationFragment(), initiator);
552    }
553
554    public void showSideFragment(Fragment f, int initiator) {
555        mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_SHOW);
556        mSidePanelContainer.setKeyDispatchable(true);
557
558        Bundle bundle = new Bundle();
559        bundle.putInt(BaseSideFragment.KEY_INITIATOR, initiator);
560        f.setArguments(bundle);
561        FragmentTransaction ft = getFragmentManager().beginTransaction();
562        ft.add(R.id.right_panel, f);
563        ft.addToBackStack(null);
564        ft.commit();
565
566        mHideSideFragment.showAndHide();
567    }
568
569    public void popFragmentBackStack() {
570        if (getFragmentManager().getBackStackEntryCount() > 1) {
571            getFragmentManager().popBackStack();
572        } else if (getFragmentManager().getBackStackEntryCount() == 1
573                && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_RESET) {
574            if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) {
575                mSidePanelContainer.setKeyDispatchable(false);
576                mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE);
577                mHideSideFragment.hideImmediately(true);
578            } else {
579                // It is during fade-out animation.
580            }
581        } else {
582            getFragmentManager().popBackStack();
583        }
584    }
585
586    public void onSideFragmentCanceled(int initiator) {
587        if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_RESET) {
588            return;
589        }
590        if (initiator == BaseSideFragment.INITIATOR_MENU) {
591            displayMainMenu(false);
592        }
593    }
594
595    private void resetSideFragment() {
596        while (true) {
597            if (!getFragmentManager().popBackStackImmediate()) {
598                break;
599            }
600        }
601        mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET);
602    }
603
604    @Override
605    public void onActivityResult(int requestCode, int resultCode, Intent data) {
606        if (resultCode == Activity.RESULT_OK) {
607            switch (requestCode) {
608                case REQUEST_START_SETUP_ACTIIVTY:
609                    if (mTvInputForSetup != null) {
610                        mInitTvInputId = mTvInputForSetup.getId();
611                    }
612                    break;
613
614                default:
615                    //TODO: Handle failure of setup.
616            }
617        } else {
618            mChildActivityCanceled = true;
619        }
620        mTvInputForSetup = null;
621    }
622
623    @Override
624    public boolean dispatchKeyEvent(KeyEvent event) {
625        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
626        if (mMainMenuView.isShown() || getFragmentManager().getBackStackEntryCount() > 0) {
627            return super.dispatchKeyEvent(event);
628        }
629        int eventKeyCode = event.getKeyCode();
630        for (int keycode : KEYCODE_BLACKLIST) {
631            if (keycode == eventKeyCode) {
632                return super.dispatchKeyEvent(event);
633            }
634        }
635        return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event);
636    }
637
638    @Override
639    public void onAudioFocusChange(int focusChange) {
640        mAudioFocusStatus = focusChange;
641        setVolumeByAudioFocusStatus();
642    }
643
644    private void setVolumeByAudioFocusStatus() {
645        if (mTvView.isPlaying()) {
646            switch (mAudioFocusStatus) {
647                case AudioManager.AUDIOFOCUS_GAIN:
648                    if (!mSilenceRequired) {
649                        mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
650                        if (isShyModeSet()) {
651                            setShynessMode(false);
652                        }
653                    } else {
654                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
655                    }
656                    break;
657                case AudioManager.AUDIOFOCUS_LOSS:
658                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
659                    mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
660                    if (!mActivityResumed) {
661                        mSilenceRequired = true;
662                    }
663                    break;
664                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
665                    if (!mSilenceRequired) {
666                        mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
667                    } else {
668                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
669                    }
670                    break;
671            }
672        }
673        // When the activity loses the audio focus, set the Shy mode regardless of the play status.
674        if (mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS ||
675                mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
676            if (!isShyModeSet()) {
677                setShynessMode(true);
678            }
679        }
680    }
681
682    private void startTvWithLastWatchedChannel(TvInput input) {
683        long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId());
684        startTv(input, channelId);
685    }
686
687    private void startTv(TvInput input, long channelId) {
688        if (mChannelMap != null) {
689            // TODO: when this case occurs, we should remove the case.
690            Log.w(TAG, "The previous variables are not released in startTv");
691            stopTv();
692        }
693
694        mMainMenuView.setChannelMap(null);
695        mChannelNumberView.setChannels(null);
696        int result = mAudioManager.requestAudioFocus(TvActivity.this,
697                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
698        mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
699                        AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
700
701        // Prepare a new channel map for the current input.
702        mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished);
703        mTvView.start(mTvInputManagerHelper);
704        setVolumeByAudioFocusStatus();
705        tune();
706    }
707
708    private void stopTv() {
709        if (mTvView.isPlaying()) {
710            mTvView.stop();
711            mAudioManager.abandonAudioFocus(this);
712        }
713        if (mChannelMap != null) {
714            mMainMenuView.setChannelMap(null);
715            mChannelNumberView.setChannels(null);
716            mChannelMap.close();
717            mChannelMap = null;
718        }
719        mTunePendding = false;
720
721        if (!isShyModeSet()) {
722            setShynessMode(true);
723        }
724    }
725
726    private boolean isPlaying() {
727        return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID;
728    }
729
730    private void startPip() {
731        if (mPipChannelId == Channel.INVALID_ID) {
732            Log.w(TAG, "PIP channel id is an invalid id.");
733            return;
734        }
735        if (DEBUG) Log.d(TAG, "startPip()");
736        mPipView.start(mTvInputManagerHelper);
737        boolean success = mPipView.tuneTo(mPipChannelId, new OnTuneListener() {
738            @Override
739            public void onUnexpectedStop(long channelId) {
740                Log.w(TAG, "The PIP is Unexpectedly stopped");
741                enablePipView(false);
742            }
743
744            @Override
745            public void onTuned(boolean success, long channelId) {
746                if (!success) {
747                    Log.w(TAG, "Fail to start the PIP during channel tunning");
748                    enablePipView(false);
749                } else {
750                    mPipView.setVisibility(View.VISIBLE);
751                }
752            }
753
754            @Override
755            public void onStreamInfoChanged(StreamInfo info) {
756                // Do nothing.
757            }
758
759            @Override
760            public void onChannelChanged(Uri channel) {
761                // Do nothing.
762            }
763        });
764        if (!success) {
765            Log.w(TAG, "Fail to start the PIP");
766            return;
767        }
768        mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
769    }
770
771    private void stopPip() {
772        if (DEBUG) Log.d(TAG, "stopPip");
773        if (mPipView.isPlaying()) {
774            mPipView.setVisibility(View.INVISIBLE);
775            mPipView.stop();
776        }
777    }
778
779    private final Runnable mOnChannelsLoadFinished = new Runnable() {
780        @Override
781        public void run() {
782            if (mTunePendding) {
783                tune();
784            }
785
786            mChannelNumberView.setChannels(
787                    (mChannelMap == null) ? null : mChannelMap.getChannelList(false));
788            mMainMenuView.setChannelMap(mChannelMap);
789        }
790    };
791
792    private void tune() {
793        if (DEBUG) Log.d(TAG, "tune()");
794        // Prerequisites to be able to tune.
795        if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
796            if (DEBUG) Log.d(TAG, "Channel map not ready");
797            mTunePendding = true;
798            return;
799        }
800        mTunePendding = false;
801        long channelId = mChannelMap.getCurrentChannelId();
802        final String inputId = mChannelMap.getTvInput().getId();
803        if (channelId == Channel.INVALID_ID) {
804            stopTv();
805            Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
806            return;
807        }
808
809        mTvView.tuneTo(channelId, new OnTuneListener() {
810            @Override
811            public void onUnexpectedStop(long channelId) {
812                stopTv();
813                startTv(Channel.INVALID_ID);
814            }
815
816            @Override
817            public void onTuned(boolean success, long channelId) {
818                if (!success) {
819                    Log.w(TAG, "Failed to tune to channel " + channelId);
820                    // TODO: show something to user about this error.
821                } else {
822                    Utils.setLastWatchedChannelId(TvActivity.this, inputId,
823                            mTvView.getCurrentTvInputInfo().getId(), channelId);
824                }
825            }
826
827            @Override
828            public void onStreamInfoChanged(StreamInfo info) {
829                updateChannelBanner(false);
830                applyDisplayMode(info.getVideoWidth(), info.getVideoHeight());
831            }
832
833            @Override
834            public void onChannelChanged(Uri channel) {
835                // TODO: update {@code mChannelMap} and the banner.
836            }
837        });
838        updateChannelBanner(true);
839        if (mChildActivityCanceled) {
840            displayMainMenu(false);
841            mChildActivityCanceled = false;
842        }
843        if (isShyModeSet()) {
844            setShynessMode(false);
845            // TODO: Set the shy mode to true when tune() fails.
846        }
847    }
848
849    public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner,
850            boolean hideSidePanel) {
851        hideOverlays(hideMainMenu, hideChannelBanner, hideSidePanel, true);
852    }
853
854    public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner,
855            boolean hideSidePanel, boolean withAnimation) {
856        if (hideMainMenu) {
857            mHideMainMenu.hideImmediately(withAnimation);
858        }
859        if (hideChannelBanner) {
860            mHideChannelBanner.hideImmediately(withAnimation);
861        }
862        if (hideSidePanel) {
863            if ((Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) {
864                return;
865            }
866            mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE);
867            mHideSideFragment.hideImmediately(withAnimation);
868        }
869    }
870
871    private void applyDisplayMode(int videoWidth, int videoHeight) {
872        int decorViewWidth = getWindow().getDecorView().getWidth();
873        int decorViewHeight = getWindow().getDecorView().getHeight();
874        ViewGroup.LayoutParams layoutParams = mTvView.getLayoutParams();
875        int displayMode = mDisplayMode;
876        double decorViewRatio = 0;
877        double videoRatio = 0;
878        if (decorViewWidth <= 0 || decorViewHeight <= 0 || videoWidth <= 0 || videoHeight <=0) {
879            displayMode = DisplayMode.MODE_FULL;
880            Log.w(TAG, "Some resolution info is missing during applyDisplayMode. ("
881                    + "decorViewWidth=" + decorViewWidth + ", decorViewHeight=" + decorViewHeight
882                    + ", width=" + videoWidth + ", height=" + videoHeight + ")");
883        } else {
884            decorViewRatio = (double) decorViewWidth / decorViewHeight;
885            videoRatio = (double) videoWidth / videoHeight;
886        }
887        if (displayMode == DisplayMode.MODE_FULL) {
888            layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
889            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
890        } else if (displayMode == DisplayMode.MODE_ZOOM) {
891            if (videoRatio < decorViewRatio) {
892                // Y axis will be clipped.
893                layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
894                layoutParams.height = (int) (decorViewWidth / videoRatio);
895            } else {
896                // X axis will be clipped.
897                layoutParams.width = (int) (decorViewHeight * videoRatio);
898                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
899            }
900        } else {
901            // MODE_NORMAL (default value)
902            if (videoRatio < decorViewRatio) {
903                // X axis has black area.
904                layoutParams.width = (int) (decorViewHeight * videoRatio);
905                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
906            } else {
907                // Y axis has black area.
908                layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
909                layoutParams.height = (int) (decorViewWidth / videoRatio);
910            }
911        }
912        mTvView.setLayoutParams(layoutParams);
913    }
914
915    private void updateChannelBanner(final boolean showBanner) {
916        runOnUiThread(new Runnable() {
917            @Override
918            public void run() {
919                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
920                    return;
921                }
922
923                mChannelBanner.updateViews(mChannelMap, mTvView);
924                if (showBanner) {
925                    mHideChannelBanner.showAndHide();
926                }
927            }
928        });
929    }
930
931    private void displayMainMenu(final boolean resetSelectedItemPosition) {
932        runOnUiThread(new Runnable() {
933            @Override
934            public void run() {
935                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
936                    return;
937                }
938
939                if (!mMainMenuView.isShown() && resetSelectedItemPosition) {
940                    mMainMenuView.resetSelectedItemPosition();
941                }
942                mHideMainMenu.showAndHide();
943            }
944        });
945    }
946
947    public void showRecentlyWatchedDialog() {
948        showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
949                new RecentlyWatchedDialogFragment());
950    }
951
952    @Override
953    protected void onSaveInstanceState(Bundle outState) {
954        // Do not save instance state because restoring instance state when TV app died
955        // unexpectedly can cause some problems like initializing fragments duplicately and
956        // accessing resource before it is initialzed.
957    }
958
959    @Override
960    protected void onDestroy() {
961        if (DEBUG) Log.d(TAG, "onDestroy()");
962        mTvInputManagerHelper.stop();
963        super.onDestroy();
964    }
965
966    @Override
967    public boolean onKeyDown(int keyCode, KeyEvent event) {
968        if (mMainMenuView.getVisibility() == View.VISIBLE
969                || getFragmentManager().getBackStackEntryCount() > 0) {
970            return super.onKeyDown(keyCode, event);
971        }
972        if (mChannelNumberView.isShown()) {
973            return mChannelNumberView.onKeyDown(keyCode, event);
974        }
975        if (mChannelMap == null) {
976            return false;
977        }
978        switch (keyCode) {
979            case KeyEvent.KEYCODE_CHANNEL_UP:
980            case KeyEvent.KEYCODE_DPAD_UP:
981                channelUp();
982                return true;
983
984            case KeyEvent.KEYCODE_CHANNEL_DOWN:
985            case KeyEvent.KEYCODE_DPAD_DOWN:
986                channelDown();
987                return true;
988        }
989        return super.onKeyDown(keyCode, event);
990    }
991
992    @Override
993    public boolean onKeyUp(int keyCode, KeyEvent event) {
994        if (getFragmentManager().getBackStackEntryCount() > 0) {
995            if (keyCode == KeyEvent.KEYCODE_BACK) {
996                popFragmentBackStack();
997                return true;
998            }
999            return super.onKeyUp(keyCode, event);
1000        }
1001        if (mMainMenuView.isShown() || mChannelBanner.isShown()) {
1002            if (keyCode == KeyEvent.KEYCODE_BACK) {
1003                hideOverlays(true, true, false);
1004                return true;
1005            }
1006            if (mMainMenuView.isShown()) {
1007                return super.onKeyUp(keyCode, event);
1008            }
1009        }
1010        if (mChannelNumberView.isShown()) {
1011            if (keyCode == KeyEvent.KEYCODE_BACK) {
1012                mChannelNumberView.hide();
1013                return true;
1014            }
1015            return mChannelNumberView.onKeyUp(keyCode, event);
1016        }
1017        if (keyCode == KeyEvent.KEYCODE_BACK) {
1018            // When the event is from onUnhandledInputEvent, onBackPressed is not automatically
1019            // called. Therefore, we need to explicitly call onBackPressed().
1020            onBackPressed();
1021            return true;
1022        }
1023
1024        if (mHandler.hasMessages(MSG_START_TV_RETRY)) {
1025            // Ignore key events during startTv retry.
1026            return true;
1027        }
1028        if (mChannelMap == null) {
1029            switch (keyCode) {
1030                case KeyEvent.KEYCODE_H:
1031                    showRecentlyWatchedDialog();
1032                    return true;
1033                case KeyEvent.KEYCODE_TV_INPUT:
1034                case KeyEvent.KEYCODE_I:
1035                case KeyEvent.KEYCODE_CHANNEL_UP:
1036                case KeyEvent.KEYCODE_DPAD_UP:
1037                case KeyEvent.KEYCODE_CHANNEL_DOWN:
1038                case KeyEvent.KEYCODE_DPAD_DOWN:
1039                case KeyEvent.KEYCODE_NUMPAD_ENTER:
1040                case KeyEvent.KEYCODE_DPAD_CENTER:
1041                case KeyEvent.KEYCODE_E:
1042                case KeyEvent.KEYCODE_MENU:
1043                    showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
1044                    return true;
1045            }
1046        } else {
1047            if (ChannelNumberView.isChannelNumberKey(keyCode)
1048                    && keyCode != KeyEvent.KEYCODE_MINUS) {
1049                mChannelNumberView.show();
1050                mHideChannelBanner.hideImmediately(true);
1051                return mChannelNumberView.onKeyUp(keyCode, event);
1052            }
1053            switch (keyCode) {
1054                case KeyEvent.KEYCODE_H:
1055                    showRecentlyWatchedDialog();
1056                    return true;
1057
1058                case KeyEvent.KEYCODE_TV_INPUT:
1059                case KeyEvent.KEYCODE_I:
1060                    showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
1061                    return true;
1062
1063                case KeyEvent.KEYCODE_DPAD_LEFT:
1064                case KeyEvent.KEYCODE_DPAD_RIGHT:
1065                    displayMainMenu(true);
1066                    return true;
1067
1068                case KeyEvent.KEYCODE_ENTER:
1069                case KeyEvent.KEYCODE_NUMPAD_ENTER:
1070                case KeyEvent.KEYCODE_E:
1071                case KeyEvent.KEYCODE_DPAD_CENTER:
1072                case KeyEvent.KEYCODE_MENU:
1073                    if (event.isCanceled()) {
1074                        return true;
1075                    }
1076                    if (keyCode != KeyEvent.KEYCODE_MENU) {
1077                        updateChannelBanner(true);
1078                    }
1079                    if (keyCode != KeyEvent.KEYCODE_E) {
1080                        displayMainMenu(true);
1081                    }
1082                    return true;
1083            }
1084        }
1085        if (USE_DEBUG_KEYS) {
1086            switch (keyCode) {
1087                case KeyEvent.KEYCODE_W: {
1088                    mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
1089                    if (mDebugNonFullSizeScreen) {
1090                        mTvView.layout(100, 100, 400, 300);
1091                    } else {
1092                        ViewGroup.LayoutParams params = mTvView.getLayoutParams();
1093                        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
1094                        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
1095                        mTvView.setLayoutParams(params);
1096                    }
1097                    return true;
1098                }
1099                case KeyEvent.KEYCODE_P: {
1100                    togglePipView();
1101                    return true;
1102                }
1103                case KeyEvent.KEYCODE_CTRL_LEFT:
1104                case KeyEvent.KEYCODE_CTRL_RIGHT: {
1105                    mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
1106                    return true;
1107                }
1108                case KeyEvent.KEYCODE_O: {
1109                    showDisplayModeOption(BaseSideFragment.INITIATOR_SHORTCUT_KEY);
1110                    return true;
1111                }
1112            }
1113        }
1114        return super.onKeyUp(keyCode, event);
1115    }
1116
1117    @Override
1118    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
1119        if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event);
1120        // Treat the BACK key long press as the normal press since we changed the behavior in
1121        // onBackPressed().
1122        if (keyCode == KeyEvent.KEYCODE_BACK) {
1123            super.onBackPressed();
1124            return true;
1125        }
1126        return false;
1127    }
1128
1129    @Override
1130    public void onBackPressed() {
1131        if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) {
1132            // TODO: show the following toast message in the future.
1133//            Toast.makeText(getApplicationContext(), getResources().getString(
1134//                    R.string.long_press_back), Toast.LENGTH_SHORT).show();
1135
1136            // If back key would exit TV app,
1137            // show McLauncher instead so we can get benefit of McLauncher's shyMode.
1138            Intent startMain = new Intent(Intent.ACTION_MAIN);
1139            startMain.addCategory(Intent.CATEGORY_HOME);
1140            startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1141            startActivity(startMain);
1142        } else {
1143            super.onBackPressed();
1144        }
1145    }
1146
1147    @Override
1148    public void onUserInteraction() {
1149        super.onUserInteraction();
1150        if (mHideMainMenu.hasFocus()
1151                && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) {
1152            mHideMainMenu.showAndHide();
1153        }
1154        if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) {
1155            mHideSideFragment.showAndHide();
1156        }
1157    }
1158
1159    @Override
1160    public boolean onTouchEvent(MotionEvent event) {
1161        if (mMainMenuView.getVisibility() != View.VISIBLE) {
1162            mGestureDetector.onTouchEvent(event);
1163        }
1164        return super.onTouchEvent(event);
1165    }
1166
1167    public void togglePipView() {
1168        enablePipView(!mPipEnabled);
1169    }
1170
1171    public void enablePipView(boolean enable) {
1172        if (enable == mPipEnabled) {
1173            return;
1174        }
1175        if (enable) {
1176            long pipChannelId = mTvView.getCurrentChannelId();
1177            if (pipChannelId != Channel.INVALID_ID) {
1178                mPipEnabled = true;
1179                mPipChannelId = pipChannelId;
1180                startPip();
1181            }
1182        } else {
1183            mPipEnabled = false;
1184            mPipChannelId = Channel.INVALID_ID;
1185            stopPip();
1186        }
1187    }
1188
1189    private boolean dispatchKeyEventToSession(final KeyEvent event) {
1190        if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
1191        if (mTvView != null) {
1192            return mTvView.dispatchKeyEvent(event);
1193        }
1194        return false;
1195    }
1196
1197    public void moveToChannel(long id) {
1198        if (mChannelMap != null && mChannelMap.isLoadFinished()
1199                && id != mChannelMap.getCurrentChannelId()) {
1200            if (mChannelMap.moveToChannel(id)) {
1201                tune();
1202            } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) {
1203                startTv(id);
1204            } else {
1205                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1206            }
1207        }
1208    }
1209
1210    private void channelUp() {
1211        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
1212            if (mChannelMap.moveToNextChannel()) {
1213                tune();
1214            } else {
1215                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1216            }
1217        }
1218    }
1219
1220    private void channelDown() {
1221        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
1222            if (mChannelMap.moveToPreviousChannel()) {
1223                tune();
1224            } else {
1225                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1226            }
1227        }
1228    }
1229
1230    public void showDialogFragment(final String tag, final DialogFragment dialog) {
1231        // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV.
1232        if (!AVAILABLE_DIALOG_TAGS.contains(tag)) {
1233            return;
1234        }
1235        mHandler.post(new Runnable() {
1236            @Override
1237            public void run() {
1238                FragmentManager fm = getFragmentManager();
1239                fm.executePendingTransactions();
1240
1241                for (String availableTag : AVAILABLE_DIALOG_TAGS) {
1242                    if (fm.findFragmentByTag(availableTag) != null) {
1243                        return;
1244                    }
1245                }
1246
1247                FragmentTransaction ft = getFragmentManager().beginTransaction();
1248                ft.addToBackStack(null);
1249                dialog.show(ft, tag);
1250            }
1251        });
1252    }
1253
1254    public boolean isClosedCaptionEnabled() {
1255        return mIsClosedCaptionEnabled;
1256    }
1257
1258    public void setClosedCaptionEnabled(boolean enable, boolean storeInPreference) {
1259        mIsClosedCaptionEnabled = enable;
1260        if (storeInPreference) {
1261            mSharedPreferences.edit().putBoolean(TvSettings.PREF_CLOSED_CAPTION_ENABLED, enable)
1262                    .apply();
1263        }
1264        // TODO: send the change to TIS
1265    }
1266
1267    public void restoreClosedCaptionEnabled() {
1268        setClosedCaptionEnabled(mSharedPreferences.getBoolean(
1269                TvSettings.PREF_CLOSED_CAPTION_ENABLED, false), false);
1270    }
1271
1272    // Returns a constant defined in DisplayMode.
1273    public int getDisplayMode() {
1274        return mDisplayMode;
1275    }
1276
1277    public void setDisplayMode(int displayMode, boolean storeInPreference) {
1278        mDisplayMode = displayMode;
1279        if (storeInPreference) {
1280            mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply();
1281        }
1282        applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight());
1283    }
1284
1285    public void restoreDisplayMode() {
1286        setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE,
1287                DisplayMode.MODE_NORMAL), false);
1288    }
1289
1290    // Returns a constant defined in TvSettings.
1291    public int getPipLocation() {
1292        return mPipLocation;
1293    }
1294
1295    public void setPipLocation(int pipLocation, boolean storeInPreference) {
1296        mPipLocation = pipLocation;
1297        if (storeInPreference) {
1298            mSharedPreferences.edit().putInt(TvSettings.PREF_PIP_LOCATION, pipLocation).apply();
1299        }
1300        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams();
1301        if (mPipLocation == TvSettings.PIP_LOCATION_TOP_LEFT) {
1302            lp.gravity = Gravity.TOP | Gravity.LEFT;
1303        } else if (mPipLocation == TvSettings.PIP_LOCATION_TOP_RIGHT) {
1304            lp.gravity = Gravity.TOP | Gravity.RIGHT;
1305        } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_LEFT) {
1306            lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
1307        } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_RIGHT) {
1308            lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
1309        } else {
1310            throw new IllegalArgumentException("Invaild PIP location: " + pipLocation);
1311        }
1312        mPipView.setLayoutParams(lp);
1313    }
1314
1315    public void restorePipLocation() {
1316        setPipLocation(mSharedPreferences.getInt(TvSettings.PREF_PIP_LOCATION,
1317                TvSettings.PIP_LOCATION_BOTTOM_RIGHT), false);
1318    }
1319
1320    private class HideRunnable implements Runnable {
1321        private final View mView;
1322        private final long mWaitingTime;
1323        private boolean mOnHideAnimation;
1324        private final Runnable mPreShowListener;
1325        private final Runnable mPostHideListener;
1326        private boolean mHasFocusDuringHideAnimation;
1327
1328        private HideRunnable(View view, long waitingTime) {
1329            this(view, waitingTime, null, null);
1330        }
1331
1332        private HideRunnable(View view, long waitingTime, Runnable preShowListener,
1333                Runnable postHideListener) {
1334            mView = view;
1335            mWaitingTime = waitingTime;
1336            mPreShowListener = preShowListener;
1337            mPostHideListener = postHideListener;
1338        }
1339
1340        @Override
1341        public void run() {
1342            startHideAnimation(false);
1343        }
1344
1345        private boolean hasFocus() {
1346            return mView.getVisibility() == View.VISIBLE
1347                    && (!mOnHideAnimation || mHasFocusDuringHideAnimation);
1348        }
1349
1350        private void startHideAnimation(boolean fastFadeOutRequired) {
1351            mOnHideAnimation = true;
1352            mHasFocusDuringHideAnimation = !fastFadeOutRequired;
1353            Animation anim = AnimationUtils.loadAnimation(TvActivity.this,
1354                    android.R.anim.fade_out);
1355            anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this,
1356                    android.R.interpolator.fast_out_linear_in));
1357            if (fastFadeOutRequired) {
1358                anim.setDuration(mShortAnimationDuration);
1359            }
1360            anim.setAnimationListener(new Animation.AnimationListener() {
1361                @Override
1362                public void onAnimationStart(Animation animation) {
1363                }
1364
1365                @Override
1366                public void onAnimationRepeat(Animation animation) {
1367                }
1368
1369                @Override
1370                public void onAnimationEnd(Animation animation) {
1371                    if (mOnHideAnimation) {
1372                        hideView();
1373                    }
1374                }
1375            });
1376
1377            mView.clearAnimation();
1378            mView.startAnimation(anim);
1379        }
1380
1381        private void hideView() {
1382            mOnHideAnimation = false;
1383            mHasFocusDuringHideAnimation = false;
1384            mView.setVisibility(View.GONE);
1385            if (mPostHideListener != null) {
1386                mPostHideListener.run();
1387            }
1388        }
1389
1390        private void hideImmediately(boolean withAnimation) {
1391            if (mView.getVisibility() != View.VISIBLE) {
1392                return;
1393            }
1394            if (!withAnimation) {
1395                mHandler.removeCallbacks(this);
1396                hideView();
1397                mView.clearAnimation();
1398                return;
1399            }
1400            if (!mOnHideAnimation) {
1401                mHandler.removeCallbacks(this);
1402                startHideAnimation(true);
1403            }
1404        }
1405
1406        private void showAndHide() {
1407            if (mView.getVisibility() != View.VISIBLE) {
1408                if (mPreShowListener != null) {
1409                    mPreShowListener.run();
1410                }
1411                mView.setVisibility(View.VISIBLE);
1412                Animation anim = AnimationUtils.loadAnimation(TvActivity.this,
1413                        android.R.anim.fade_in);
1414                anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this,
1415                        android.R.interpolator.linear_out_slow_in));
1416                mView.clearAnimation();
1417                mView.startAnimation(anim);
1418            }
1419            // Schedule the hide animation after a few seconds.
1420            mHandler.removeCallbacks(this);
1421            if (mOnHideAnimation) {
1422                mOnHideAnimation = false;
1423                mView.clearAnimation();
1424                mView.setAlpha(1f);
1425            }
1426            mHandler.postDelayed(this, mWaitingTime);
1427        }
1428    }
1429
1430    private void setShynessMode(boolean shyMode) {
1431        mIsShy = shyMode;
1432        setMediaPlaying(!shyMode);
1433    }
1434
1435    private boolean isShyModeSet() {
1436        return mIsShy;
1437    }
1438
1439    @Override
1440    public void onStopMediaPlaying() {
1441        mHandler.removeMessages(MSG_START_TV_RETRY);
1442        stopTv();
1443        stopPip();
1444        super.onStopMediaPlaying();
1445    }
1446}
1447