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