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