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