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