TvActivity.java revision 829ca34b57d208a84430b2ccecd8975598d086da
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 String LEANBACK_SET_SHYNESS_BROADCAST =
116            "com.android.mclauncher.action.SET_APP_SHYNESS";
117    private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode";
118
119    private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>();
120
121    private TvInputManager mTvInputManager;
122    private TunableTvView mTvView;
123    private LinearLayout mControlGuide;
124    private MainMenuView mMainMenuView;
125    private ChannelBannerView mChannelBanner;
126    private ChannelNumberView mChannelNumberView;
127    private SidePanelContainer mSidePanelContainer;
128    private HideRunnable mHideChannelBanner;
129    private HideRunnable mHideControlGuide;
130    private HideRunnable mHideMainMenu;
131    private HideRunnable mHideSideFragment;
132    private int mShortAnimationDuration;
133    private int mDisplayWidth;
134    private GestureDetector mGestureDetector;
135    private ChannelMap mChannelMap;
136    private long mInitChannelId;
137    private String mInitTvInputId;
138    private boolean mChildActivityCanceled;
139
140    private TvInput mTvInputForSetup;
141    private TvInputManagerHelper mTvInputManagerHelper;
142    private AudioManager mAudioManager;
143    private int mAudioFocusStatus;
144    private boolean mTunePendding;
145    private boolean mPipEnabled;
146    private long mPipChannelId;
147    private boolean mDebugNonFullSizeScreen;
148    private boolean mActivityResumed;
149    private boolean mSilenceRequired;
150    private boolean mUseKeycodeBlacklist;
151    private boolean mIsShy = true;
152
153    private boolean mIsClosedCaptionEnabled;
154    private int mDisplayMode;
155    private int mPipLocation;
156    private SharedPreferences mSharedPreferences;
157
158    private SimpleGuideFragment mSimpleGuideFragment;
159
160    static {
161        AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
162        AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG);
163    }
164
165    // PIP is used for debug/verification of multiple sessions rather than real PIP feature.
166    // When PIP is enabled, the same channel as mTvView is tuned.
167    private TunableTvView mPipView;
168
169    private final Handler mHandler = new Handler() {
170        @Override
171        public void handleMessage(Message msg) {
172            if (msg.what == MSG_START_TV_RETRY) {
173                Object[] arg = (Object[]) msg.obj;
174                TvInput input = (TvInput) arg[0];
175                long channelId = (Long) arg[1];
176                int retryCount = msg.arg1;
177                startTvIfAvailableOrRetry(input, channelId, retryCount);
178            }
179        }
180    };
181
182    @Override
183    protected void onCreate(Bundle savedInstanceState) {
184        super.onCreate(savedInstanceState);
185
186        setContentView(R.layout.activity_tv);
187        mTvView = (TunableTvView) findViewById(R.id.tv_view);
188        mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() {
189            @Override
190            public boolean onUnhandledInputEvent(InputEvent event) {
191                if (event instanceof KeyEvent) {
192                    KeyEvent keyEvent = (KeyEvent) event;
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        mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
302        mTvInputManagerHelper = new TvInputManagerHelper(mTvInputManager);
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 (!input.hasChannel(false)) {
468            mTvInputForSetup = null;
469            if (!startSetupActivity(input)) {
470                String message = String.format(
471                        getString(R.string.empty_channel_tvinput_and_no_setup_activity),
472                        input.getDisplayName());
473                Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
474                showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
475            }
476            return;
477        }
478
479        stopTv();
480        startTvWithLastWatchedChannel(input);
481    }
482
483    public TvInputManagerHelper getTvInputManagerHelper() {
484        return mTvInputManagerHelper;
485    }
486
487    public TvInput getSelectedTvInput() {
488        return mChannelMap == null ? null : mChannelMap.getTvInput();
489    }
490
491    public void showEditChannelsFragment(int initiator) {
492        showSideFragment(new EditChannelsFragment(mChannelMap.getChannelList(false)), initiator);
493    }
494
495    public boolean startSetupActivity() {
496        if (getSelectedTvInput() == null) {
497            return false;
498        }
499        return startSetupActivity(getSelectedTvInput());
500    }
501
502    public boolean startSetupActivity(TvInput input) {
503        Intent intent = input.getIntentForSetupActivity();
504        if (intent == null) {
505            return false;
506        }
507        startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY);
508        mTvInputForSetup = input;
509        mInitTvInputId = null;
510        stopTv();
511        return true;
512    }
513
514    public boolean startSettingsActivity() {
515        TvInput input = getSelectedTvInput();
516        if (input == null) {
517            Log.w(TAG, "There is no selected TV input during startSettingsActivity");
518            return false;
519        }
520        Intent intent = input.getIntentForSettingsActivity();
521        if (intent == null) {
522            return false;
523        }
524        startActivityForResult(intent, REQUEST_START_SETTINGS_ACTIIVTY);
525        return true;
526    }
527
528    public void showSimpleGuide(int initiator) {
529        mSimpleGuideFragment = new SimpleGuideFragment(this, mChannelMap);
530        showSideFragment(mSimpleGuideFragment, initiator);
531    }
532
533    public void showInputPicker(int initiator) {
534        showSideFragment(new InputPickerFragment(), initiator);
535    }
536
537    public void showDisplayModeOption(int initiator) {
538        showSideFragment(new DisplayModeOptionFragment(), initiator);
539    }
540
541    public void showClosedCaptionOption(int initiator) {
542        showSideFragment(new ClosedCaptionOptionFragment(), initiator);
543    }
544
545    public void showPipLocationOption(int initiator) {
546        showSideFragment(new PipLocationFragment(), initiator);
547    }
548
549    public void showSideFragment(Fragment f, int initiator) {
550        mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_SHOW);
551        mSidePanelContainer.setKeyDispatchable(true);
552
553        Bundle bundle = new Bundle();
554        bundle.putInt(BaseSideFragment.KEY_INITIATOR, initiator);
555        f.setArguments(bundle);
556        FragmentTransaction ft = getFragmentManager().beginTransaction();
557        ft.add(R.id.right_panel, f);
558        ft.addToBackStack(null);
559        ft.commit();
560
561        mHideSideFragment.showAndHide();
562    }
563
564    public void popFragmentBackStack() {
565        if (getFragmentManager().getBackStackEntryCount() > 1) {
566            getFragmentManager().popBackStack();
567        } else if (getFragmentManager().getBackStackEntryCount() == 1
568                && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_RESET) {
569            if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) {
570                mSidePanelContainer.setKeyDispatchable(false);
571                mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE);
572                mHideSideFragment.hideImmediately(true);
573            } else {
574                // It is during fade-out animation.
575            }
576        } else {
577            getFragmentManager().popBackStack();
578        }
579    }
580
581    public void onSideFragmentCanceled(int initiator) {
582        if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_RESET) {
583            return;
584        }
585        if (initiator == BaseSideFragment.INITIATOR_MENU) {
586            displayMainMenu(false);
587        }
588    }
589
590    private void resetSideFragment() {
591        while (true) {
592            if (!getFragmentManager().popBackStackImmediate()) {
593                break;
594            }
595        }
596        mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET);
597    }
598
599    @Override
600    public void onActivityResult(int requestCode, int resultCode, Intent data) {
601        if (resultCode == Activity.RESULT_OK) {
602            switch (requestCode) {
603                case REQUEST_START_SETUP_ACTIIVTY:
604                    if (mTvInputForSetup != null) {
605                        mInitTvInputId = mTvInputForSetup.getId();
606                    }
607                    break;
608
609                default:
610                    //TODO: Handle failure of setup.
611            }
612        } else {
613            mChildActivityCanceled = true;
614        }
615        mTvInputForSetup = null;
616    }
617
618    @Override
619    public boolean dispatchKeyEvent(KeyEvent event) {
620        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
621        if (mMainMenuView.isShown() || getFragmentManager().getBackStackEntryCount() > 0) {
622            return super.dispatchKeyEvent(event);
623        }
624        int eventKeyCode = event.getKeyCode();
625        for (int keycode : KEYCODE_BLACKLIST) {
626            if (keycode == eventKeyCode) {
627                return super.dispatchKeyEvent(event);
628            }
629        }
630        return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event);
631    }
632
633    @Override
634    public void onAudioFocusChange(int focusChange) {
635        mAudioFocusStatus = focusChange;
636        setVolumeByAudioFocusStatus();
637    }
638
639    private void setVolumeByAudioFocusStatus() {
640        if (mTvView.isPlaying()) {
641            switch (mAudioFocusStatus) {
642                case AudioManager.AUDIOFOCUS_GAIN:
643                    if (!mSilenceRequired) {
644                        mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
645                        if (isShyModeSet()) {
646                            setShynessMode(false);
647                        }
648                    } else {
649                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
650                    }
651                    break;
652                case AudioManager.AUDIOFOCUS_LOSS:
653                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
654                    mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
655                    if (!mActivityResumed) {
656                        mSilenceRequired = true;
657                    }
658                    break;
659                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
660                    if (!mSilenceRequired) {
661                        mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
662                    } else {
663                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
664                    }
665                    break;
666            }
667        }
668        // When the activity loses the audio focus, set the Shy mode regardless of the play status.
669        if (mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS ||
670                mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
671            if (!isShyModeSet()) {
672                setShynessMode(true);
673            }
674        }
675    }
676
677    private void startTvWithLastWatchedChannel(TvInput input) {
678        long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId());
679        startTv(input, channelId);
680    }
681
682    private void startTv(TvInput input, long channelId) {
683        if (mChannelMap != null) {
684            // TODO: when this case occurs, we should remove the case.
685            Log.w(TAG, "The previous variables are not released in startTv");
686            stopTv();
687        }
688
689        mMainMenuView.setChannelMap(null);
690        int result = mAudioManager.requestAudioFocus(TvActivity.this,
691                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
692        mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
693                        AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
694
695        // Prepare a new channel map for the current input.
696        mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished);
697        mTvView.start(mTvInputManagerHelper);
698        setVolumeByAudioFocusStatus();
699        tune();
700    }
701
702    private void stopTv() {
703        if (mTvView.isPlaying()) {
704            mTvView.stop();
705            mAudioManager.abandonAudioFocus(this);
706        }
707        if (mChannelMap != null) {
708            mMainMenuView.setChannelMap(null);
709            mChannelMap.close();
710            mChannelMap = null;
711        }
712        mTunePendding = false;
713
714        if (!isShyModeSet()) {
715            setShynessMode(true);
716        }
717    }
718
719    private boolean isPlaying() {
720        return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID;
721    }
722
723    private void startPip() {
724        if (mPipChannelId == Channel.INVALID_ID) {
725            Log.w(TAG, "PIP channel id is an invalid id.");
726            return;
727        }
728        if (DEBUG) Log.d(TAG, "startPip()");
729        mPipView.start(mTvInputManagerHelper);
730        boolean success = mPipView.tuneTo(mPipChannelId, new OnTuneListener() {
731            @Override
732            public void onUnexpectedStop(long channelId) {
733                Log.w(TAG, "The PIP is Unexpectedly stopped");
734                enablePipView(false);
735            }
736
737            @Override
738            public void onTuned(boolean success, long channelId) {
739                if (!success) {
740                    Log.w(TAG, "Fail to start the PIP during channel tunning");
741                    enablePipView(false);
742                } else {
743                    mPipView.setVisibility(View.VISIBLE);
744                }
745            }
746
747            @Override
748            public void onStreamInfoChanged(StreamInfo info) {
749                // Do nothing.
750            }
751        });
752        if (!success) {
753            Log.w(TAG, "Fail to start the PIP");
754            return;
755        }
756        mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
757    }
758
759    private void stopPip() {
760        if (DEBUG) Log.d(TAG, "stopPip");
761        if (mPipView.isPlaying()) {
762            mPipView.setVisibility(View.INVISIBLE);
763            mPipView.stop();
764        }
765    }
766
767    private final Runnable mOnChannelsLoadFinished = new Runnable() {
768        @Override
769        public void run() {
770            if (mTunePendding) {
771                tune();
772            }
773            mChannelNumberView.setChannels(mChannelMap.getChannelList(false));
774            mMainMenuView.setChannelMap(mChannelMap);
775        }
776    };
777
778    private void tune() {
779        if (DEBUG) Log.d(TAG, "tune()");
780        // Prerequisites to be able to tune.
781        if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
782            if (DEBUG) Log.d(TAG, "Channel map not ready");
783            mTunePendding = true;
784            return;
785        }
786        mTunePendding = false;
787        long channelId = mChannelMap.getCurrentChannelId();
788        final String inputId = mChannelMap.getTvInput().getId();
789        if (channelId == Channel.INVALID_ID) {
790            stopTv();
791            Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
792            return;
793        }
794
795        mTvView.tuneTo(channelId, new OnTuneListener() {
796            @Override
797            public void onUnexpectedStop(long channelId) {
798                stopTv();
799                startTv(Channel.INVALID_ID);
800            }
801
802            @Override
803            public void onTuned(boolean success, long channelId) {
804                if (!success) {
805                    Log.w(TAG, "Failed to tune to channel " + channelId);
806                    // TODO: show something to user about this error.
807                } else {
808                    Utils.setLastWatchedChannelId(TvActivity.this, inputId,
809                            mTvView.getCurrentTvInputInfo().getId(), channelId);
810                }
811            }
812
813            @Override
814            public void onStreamInfoChanged(StreamInfo info) {
815                updateChannelBanner(false);
816                applyDisplayMode(info.getVideoWidth(), info.getVideoHeight());
817            }
818        });
819        updateChannelBanner(true);
820        if (mChildActivityCanceled) {
821            displayMainMenu(false);
822            mChildActivityCanceled = false;
823        }
824        if (isShyModeSet()) {
825            setShynessMode(false);
826            // TODO: Set the shy mode to true when tune() fails.
827        }
828    }
829
830    public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner,
831            boolean hideSidePanel) {
832        hideOverlays(hideMainMenu, hideChannelBanner, hideSidePanel, true);
833    }
834
835    public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner,
836            boolean hideSidePanel, boolean withAnimation) {
837        if (hideMainMenu) {
838            mHideMainMenu.hideImmediately(withAnimation);
839        }
840        if (hideChannelBanner) {
841            mHideChannelBanner.hideImmediately(withAnimation);
842        }
843        if (hideSidePanel) {
844            if ((Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) {
845                return;
846            }
847            mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE);
848            mHideSideFragment.hideImmediately(withAnimation);
849        }
850    }
851
852    private void applyDisplayMode(int videoWidth, int videoHeight) {
853        int decorViewWidth = getWindow().getDecorView().getWidth();
854        int decorViewHeight = getWindow().getDecorView().getHeight();
855        ViewGroup.LayoutParams layoutParams = mTvView.getLayoutParams();
856        int displayMode = mDisplayMode;
857        double decorViewRatio = 0;
858        double videoRatio = 0;
859        if (decorViewWidth <= 0 || decorViewHeight <= 0 || videoWidth <= 0 || videoHeight <=0) {
860            displayMode = DisplayMode.MODE_FULL;
861            Log.w(TAG, "Some resolution info is missing during applyDisplayMode. ("
862                    + "decorViewWidth=" + decorViewWidth + ", decorViewHeight=" + decorViewHeight
863                    + ", width=" + videoWidth + ", height=" + videoHeight + ")");
864        } else {
865            decorViewRatio = (double) decorViewWidth / decorViewHeight;
866            videoRatio = (double) videoWidth / videoHeight;
867        }
868        if (displayMode == DisplayMode.MODE_FULL) {
869            layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
870            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
871        } else if (displayMode == DisplayMode.MODE_ZOOM) {
872            if (videoRatio < decorViewRatio) {
873                // Y axis will be clipped.
874                layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
875                layoutParams.height = (int) (decorViewWidth / videoRatio);
876            } else {
877                // X axis will be clipped.
878                layoutParams.width = (int) (decorViewHeight * videoRatio);
879                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
880            }
881        } else {
882            // MODE_NORMAL (default value)
883            if (videoRatio < decorViewRatio) {
884                // X axis has black area.
885                layoutParams.width = (int) (decorViewHeight * videoRatio);
886                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
887            } else {
888                // Y axis has black area.
889                layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
890                layoutParams.height = (int) (decorViewWidth / videoRatio);
891            }
892        }
893        mTvView.setLayoutParams(layoutParams);
894    }
895
896    private void updateChannelBanner(final boolean showBanner) {
897        runOnUiThread(new Runnable() {
898            @Override
899            public void run() {
900                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
901                    return;
902                }
903
904                mChannelBanner.updateViews(mChannelMap, mTvView);
905                if (showBanner) {
906                    mHideChannelBanner.showAndHide();
907                }
908            }
909        });
910    }
911
912    private void displayMainMenu(final boolean resetSelectedItemPosition) {
913        runOnUiThread(new Runnable() {
914            @Override
915            public void run() {
916                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
917                    return;
918                }
919
920                if (!mMainMenuView.isShown() && resetSelectedItemPosition) {
921                    mMainMenuView.resetSelectedItemPosition();
922                }
923                mHideMainMenu.showAndHide();
924            }
925        });
926    }
927
928    public void showRecentlyWatchedDialog() {
929        showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
930                new RecentlyWatchedDialogFragment());
931    }
932
933    @Override
934    protected void onSaveInstanceState(Bundle outState) {
935        // Do not save instance state because restoring instance state when TV app died
936        // unexpectedly can cause some problems like initializing fragments duplicately and
937        // accessing resource before it is initialzed.
938    }
939
940    @Override
941    protected void onDestroy() {
942        if (DEBUG) Log.d(TAG, "onDestroy()");
943        mTvInputManagerHelper.stop();
944        super.onDestroy();
945    }
946
947    @Override
948    public boolean onKeyDown(int keyCode, KeyEvent event) {
949        if (mMainMenuView.getVisibility() == View.VISIBLE
950                || getFragmentManager().getBackStackEntryCount() > 0) {
951            return super.onKeyDown(keyCode, event);
952        }
953        if (mChannelMap == null) {
954            return false;
955        }
956        switch (keyCode) {
957            case KeyEvent.KEYCODE_CHANNEL_UP:
958            case KeyEvent.KEYCODE_DPAD_UP:
959                channelUp();
960                return true;
961
962            case KeyEvent.KEYCODE_CHANNEL_DOWN:
963            case KeyEvent.KEYCODE_DPAD_DOWN:
964                channelDown();
965                return true;
966        }
967        return super.onKeyDown(keyCode, event);
968    }
969
970    @Override
971    public boolean onKeyUp(int keyCode, KeyEvent event) {
972        if (getFragmentManager().getBackStackEntryCount() > 0) {
973            if (keyCode == KeyEvent.KEYCODE_BACK) {
974                popFragmentBackStack();
975                return true;
976            }
977            return super.onKeyUp(keyCode, event);
978        }
979        if (mMainMenuView.isShown() || mChannelBanner.isShown()) {
980            if (keyCode == KeyEvent.KEYCODE_BACK) {
981                hideOverlays(true, true, false);
982                return true;
983            }
984            if (mMainMenuView.isShown()) {
985                return super.onKeyUp(keyCode, event);
986            }
987        }
988        if (mChannelNumberView.isShown()) {
989            if (keyCode == KeyEvent.KEYCODE_BACK) {
990                mChannelNumberView.hide();
991                return true;
992            }
993            return mChannelNumberView.onKeyUp(keyCode, event);
994        }
995        if (keyCode == KeyEvent.KEYCODE_BACK) {
996            // When the event is from onUnhandledInputEvent, onBackPressed is not automatically
997            // called. Therefore, we need to explicitly call onBackPressed().
998            onBackPressed();
999            return true;
1000        }
1001
1002        if (mHandler.hasMessages(MSG_START_TV_RETRY)) {
1003            // Ignore key events during startTv retry.
1004            return true;
1005        }
1006        if (mChannelMap == null) {
1007            switch (keyCode) {
1008                case KeyEvent.KEYCODE_H:
1009                    showRecentlyWatchedDialog();
1010                    return true;
1011                case KeyEvent.KEYCODE_TV_INPUT:
1012                case KeyEvent.KEYCODE_I:
1013                case KeyEvent.KEYCODE_CHANNEL_UP:
1014                case KeyEvent.KEYCODE_DPAD_UP:
1015                case KeyEvent.KEYCODE_CHANNEL_DOWN:
1016                case KeyEvent.KEYCODE_DPAD_DOWN:
1017                case KeyEvent.KEYCODE_NUMPAD_ENTER:
1018                case KeyEvent.KEYCODE_DPAD_CENTER:
1019                case KeyEvent.KEYCODE_E:
1020                case KeyEvent.KEYCODE_MENU:
1021                    showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
1022                    return true;
1023            }
1024        } else {
1025            if (ChannelNumberView.isChannelNumberKey(keyCode)
1026                    && keyCode != KeyEvent.KEYCODE_MINUS) {
1027                mChannelNumberView.show();
1028                mHideChannelBanner.hideImmediately(true);
1029                return mChannelNumberView.onKeyUp(keyCode, event);
1030            }
1031            switch (keyCode) {
1032                case KeyEvent.KEYCODE_H:
1033                    showRecentlyWatchedDialog();
1034                    return true;
1035
1036                case KeyEvent.KEYCODE_TV_INPUT:
1037                case KeyEvent.KEYCODE_I:
1038                    showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN);
1039                    return true;
1040
1041                case KeyEvent.KEYCODE_DPAD_LEFT:
1042                case KeyEvent.KEYCODE_DPAD_RIGHT:
1043                    displayMainMenu(true);
1044                    return true;
1045
1046                case KeyEvent.KEYCODE_ENTER:
1047                case KeyEvent.KEYCODE_NUMPAD_ENTER:
1048                case KeyEvent.KEYCODE_E:
1049                case KeyEvent.KEYCODE_DPAD_CENTER:
1050                case KeyEvent.KEYCODE_MENU:
1051                    if (event.isCanceled()) {
1052                        return true;
1053                    }
1054                    if (keyCode != KeyEvent.KEYCODE_MENU) {
1055                        updateChannelBanner(true);
1056                    }
1057                    if (keyCode != KeyEvent.KEYCODE_E) {
1058                        displayMainMenu(true);
1059                    }
1060                    return true;
1061            }
1062        }
1063        if (USE_DEBUG_KEYS) {
1064            switch (keyCode) {
1065                case KeyEvent.KEYCODE_W: {
1066                    mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
1067                    if (mDebugNonFullSizeScreen) {
1068                        mTvView.layout(100, 100, 400, 300);
1069                    } else {
1070                        ViewGroup.LayoutParams params = mTvView.getLayoutParams();
1071                        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
1072                        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
1073                        mTvView.setLayoutParams(params);
1074                    }
1075                    return true;
1076                }
1077                case KeyEvent.KEYCODE_P: {
1078                    togglePipView();
1079                    return true;
1080                }
1081                case KeyEvent.KEYCODE_CTRL_LEFT:
1082                case KeyEvent.KEYCODE_CTRL_RIGHT: {
1083                    mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
1084                    return true;
1085                }
1086                case KeyEvent.KEYCODE_O: {
1087                    showDisplayModeOption(BaseSideFragment.INITIATOR_SHORTCUT_KEY);
1088                    return true;
1089                }
1090            }
1091        }
1092        return super.onKeyUp(keyCode, event);
1093    }
1094
1095    @Override
1096    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
1097        if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event);
1098        // Treat the BACK key long press as the normal press since we changed the behavior in
1099        // onBackPressed().
1100        if (keyCode == KeyEvent.KEYCODE_BACK) {
1101            super.onBackPressed();
1102            return true;
1103        }
1104        return false;
1105    }
1106
1107    @Override
1108    public void onBackPressed() {
1109        if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) {
1110            // TODO: show the following toast message in the future.
1111//            Toast.makeText(getApplicationContext(), getResources().getString(
1112//                    R.string.long_press_back), Toast.LENGTH_SHORT).show();
1113
1114            // If back key would exit TV app,
1115            // show McLauncher instead so we can get benefit of McLauncher's shyMode.
1116            Intent startMain = new Intent(Intent.ACTION_MAIN);
1117            startMain.addCategory(Intent.CATEGORY_HOME);
1118            startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1119            startActivity(startMain);
1120        } else {
1121            super.onBackPressed();
1122        }
1123    }
1124
1125    @Override
1126    public void onUserInteraction() {
1127        super.onUserInteraction();
1128        if (mHideMainMenu.hasFocus()
1129                && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) {
1130            mHideMainMenu.showAndHide();
1131        }
1132        if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) {
1133            mHideSideFragment.showAndHide();
1134        }
1135    }
1136
1137    @Override
1138    public boolean onTouchEvent(MotionEvent event) {
1139        if (mMainMenuView.getVisibility() != View.VISIBLE) {
1140            mGestureDetector.onTouchEvent(event);
1141        }
1142        return super.onTouchEvent(event);
1143    }
1144
1145    public void togglePipView() {
1146        enablePipView(!mPipEnabled);
1147    }
1148
1149    public void enablePipView(boolean enable) {
1150        if (enable == mPipEnabled) {
1151            return;
1152        }
1153        if (enable) {
1154            long pipChannelId = mTvView.getCurrentChannelId();
1155            if (pipChannelId != Channel.INVALID_ID) {
1156                mPipEnabled = true;
1157                mPipChannelId = pipChannelId;
1158                startPip();
1159            }
1160        } else {
1161            mPipEnabled = false;
1162            mPipChannelId = Channel.INVALID_ID;
1163            stopPip();
1164        }
1165    }
1166
1167    private boolean dispatchKeyEventToSession(final KeyEvent event) {
1168        if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
1169        if (mTvView != null) {
1170            return mTvView.dispatchKeyEvent(event);
1171        }
1172        return false;
1173    }
1174
1175    public void moveToChannel(long id) {
1176        if (mChannelMap != null && mChannelMap.isLoadFinished()
1177                && id != mChannelMap.getCurrentChannelId()) {
1178            if (mChannelMap.moveToChannel(id)) {
1179                tune();
1180            } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) {
1181                startTv(id);
1182            } else {
1183                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1184            }
1185        }
1186    }
1187
1188    private void channelUp() {
1189        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
1190            if (mChannelMap.moveToNextChannel()) {
1191                tune();
1192            } else {
1193                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1194            }
1195        }
1196    }
1197
1198    private void channelDown() {
1199        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
1200            if (mChannelMap.moveToPreviousChannel()) {
1201                tune();
1202            } else {
1203                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
1204            }
1205        }
1206    }
1207
1208    public void showDialogFragment(final String tag, final DialogFragment dialog) {
1209        // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV.
1210        if (!AVAILABLE_DIALOG_TAGS.contains(tag)) {
1211            return;
1212        }
1213        mHandler.post(new Runnable() {
1214            @Override
1215            public void run() {
1216                FragmentManager fm = getFragmentManager();
1217                fm.executePendingTransactions();
1218
1219                for (String availableTag : AVAILABLE_DIALOG_TAGS) {
1220                    if (fm.findFragmentByTag(availableTag) != null) {
1221                        return;
1222                    }
1223                }
1224
1225                FragmentTransaction ft = getFragmentManager().beginTransaction();
1226                ft.addToBackStack(null);
1227                dialog.show(ft, tag);
1228            }
1229        });
1230    }
1231
1232    public boolean isClosedCaptionEnabled() {
1233        return mIsClosedCaptionEnabled;
1234    }
1235
1236    public void setClosedCaptionEnabled(boolean enable, boolean storeInPreference) {
1237        mIsClosedCaptionEnabled = enable;
1238        if (storeInPreference) {
1239            mSharedPreferences.edit().putBoolean(TvSettings.PREF_CLOSED_CAPTION_ENABLED, enable)
1240                    .apply();
1241        }
1242        // TODO: send the change to TIS
1243    }
1244
1245    public void restoreClosedCaptionEnabled() {
1246        setClosedCaptionEnabled(mSharedPreferences.getBoolean(
1247                TvSettings.PREF_CLOSED_CAPTION_ENABLED, false), false);
1248    }
1249
1250    // Returns a constant defined in DisplayMode.
1251    public int getDisplayMode() {
1252        return mDisplayMode;
1253    }
1254
1255    public void setDisplayMode(int displayMode, boolean storeInPreference) {
1256        mDisplayMode = displayMode;
1257        if (storeInPreference) {
1258            mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply();
1259        }
1260        applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight());
1261    }
1262
1263    public void restoreDisplayMode() {
1264        setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE,
1265                DisplayMode.MODE_NORMAL), false);
1266    }
1267
1268    // Returns a constant defined in TvSettings.
1269    public int getPipLocation() {
1270        return mPipLocation;
1271    }
1272
1273    public void setPipLocation(int pipLocation, boolean storeInPreference) {
1274        mPipLocation = pipLocation;
1275        if (storeInPreference) {
1276            mSharedPreferences.edit().putInt(TvSettings.PREF_PIP_LOCATION, pipLocation).apply();
1277        }
1278        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams();
1279        if (mPipLocation == TvSettings.PIP_LOCATION_TOP_LEFT) {
1280            lp.gravity = Gravity.TOP | Gravity.LEFT;
1281        } else if (mPipLocation == TvSettings.PIP_LOCATION_TOP_RIGHT) {
1282            lp.gravity = Gravity.TOP | Gravity.RIGHT;
1283        } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_LEFT) {
1284            lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
1285        } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_RIGHT) {
1286            lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
1287        } else {
1288            throw new IllegalArgumentException("Invaild PIP location: " + pipLocation);
1289        }
1290        mPipView.setLayoutParams(lp);
1291    }
1292
1293    public void restorePipLocation() {
1294        setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_PIP_LOCATION,
1295                TvSettings.PIP_LOCATION_BOTTOM_RIGHT), false);
1296    }
1297
1298    private class HideRunnable implements Runnable {
1299        private final View mView;
1300        private final long mWaitingTime;
1301        private boolean mOnHideAnimation;
1302        private final Runnable mPreShowListener;
1303        private final Runnable mPostHideListener;
1304        private boolean mHasFocusDuringHideAnimation;
1305
1306        private HideRunnable(View view, long waitingTime) {
1307            this(view, waitingTime, null, null);
1308        }
1309
1310        private HideRunnable(View view, long waitingTime, Runnable preShowListener,
1311                Runnable postHideListener) {
1312            mView = view;
1313            mWaitingTime = waitingTime;
1314            mPreShowListener = preShowListener;
1315            mPostHideListener = postHideListener;
1316        }
1317
1318        @Override
1319        public void run() {
1320            startHideAnimation(false);
1321        }
1322
1323        private boolean hasFocus() {
1324            return mView.getVisibility() == View.VISIBLE
1325                    && (!mOnHideAnimation || mHasFocusDuringHideAnimation);
1326        }
1327
1328        private void startHideAnimation(boolean fastFadeOutRequired) {
1329            mOnHideAnimation = true;
1330            mHasFocusDuringHideAnimation = !fastFadeOutRequired;
1331            Animation anim = AnimationUtils.loadAnimation(TvActivity.this,
1332                    android.R.anim.fade_out);
1333            anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this,
1334                    android.R.interpolator.fast_out_linear_in));
1335            if (fastFadeOutRequired) {
1336                anim.setDuration(mShortAnimationDuration);
1337            }
1338            anim.setAnimationListener(new Animation.AnimationListener() {
1339                @Override
1340                public void onAnimationStart(Animation animation) {
1341                }
1342
1343                @Override
1344                public void onAnimationRepeat(Animation animation) {
1345                }
1346
1347                @Override
1348                public void onAnimationEnd(Animation animation) {
1349                    if (mOnHideAnimation) {
1350                        hideView();
1351                    }
1352                }
1353            });
1354
1355            mView.clearAnimation();
1356            mView.startAnimation(anim);
1357        }
1358
1359        private void hideView() {
1360            mOnHideAnimation = false;
1361            mHasFocusDuringHideAnimation = false;
1362            mView.setVisibility(View.GONE);
1363            if (mPostHideListener != null) {
1364                mPostHideListener.run();
1365            }
1366        }
1367
1368        private void hideImmediately(boolean withAnimation) {
1369            if (mView.getVisibility() != View.VISIBLE) {
1370                return;
1371            }
1372            if (!withAnimation) {
1373                mHandler.removeCallbacks(this);
1374                hideView();
1375                mView.clearAnimation();
1376                return;
1377            }
1378            if (!mOnHideAnimation) {
1379                mHandler.removeCallbacks(this);
1380                startHideAnimation(true);
1381            }
1382        }
1383
1384        private void showAndHide() {
1385            if (mView.getVisibility() != View.VISIBLE) {
1386                if (mPreShowListener != null) {
1387                    mPreShowListener.run();
1388                }
1389                mView.setVisibility(View.VISIBLE);
1390                Animation anim = AnimationUtils.loadAnimation(TvActivity.this,
1391                        android.R.anim.fade_in);
1392                anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this,
1393                        android.R.interpolator.linear_out_slow_in));
1394                mView.clearAnimation();
1395                mView.startAnimation(anim);
1396            }
1397            // Schedule the hide animation after a few seconds.
1398            mHandler.removeCallbacks(this);
1399            if (mOnHideAnimation) {
1400                mOnHideAnimation = false;
1401                mView.clearAnimation();
1402                mView.setAlpha(1f);
1403            }
1404            mHandler.postDelayed(this, mWaitingTime);
1405        }
1406    }
1407
1408    private void setShynessMode(boolean shyMode) {
1409        mIsShy = shyMode;
1410        Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST);
1411        intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode);
1412        sendBroadcast(intent);
1413    }
1414
1415    private boolean isShyModeSet() {
1416        return mIsShy;
1417    }
1418}
1419