TvActivity.java revision d6903b32bf7c313e0395c492dd374bfb5b9909c8
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.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.app.DialogFragment;
24import android.app.FragmentManager;
25import android.app.FragmentTransaction;
26import android.content.ContentUris;
27import android.content.Context;
28import android.content.Intent;
29import android.graphics.Point;
30import android.media.AudioManager;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.Message;
35import android.text.TextUtils;
36import android.tv.TvInputInfo;
37import android.tv.TvInputManager;
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.widget.LinearLayout;
48import android.widget.Toast;
49
50import com.android.tv.data.Channel;
51import com.android.tv.data.ChannelMap;
52import com.android.tv.data.StreamInfo;
53import com.android.tv.dialog.EditChannelsDialogFragment;
54import com.android.tv.dialog.EditInputDialogFragment;
55import com.android.tv.dialog.InputPickerDialogFragment;
56import com.android.tv.dialog.PrivacySettingDialogFragment;
57import com.android.tv.dialog.RecentlyWatchedDialogFragment;
58import com.android.tv.input.TisTvInput;
59import com.android.tv.input.TvInput;
60import com.android.tv.input.UnifiedTvInput;
61import com.android.tv.ui.AspectRatioOptionFragment;
62import com.android.tv.ui.ChannelBannerView;
63import com.android.tv.ui.ClosedCaptionOptionFragment;
64import com.android.tv.ui.MainMenuView;
65import com.android.tv.ui.TunableTvView;
66import com.android.tv.ui.TunableTvView.OnTuneListener;
67import com.android.tv.util.TvInputManagerHelper;
68import com.android.tv.util.Utils;
69
70import java.util.HashSet;
71
72/**
73 * The main activity for demonstrating TV app.
74 */
75public class TvActivity extends Activity implements
76        InputPickerDialogFragment.InputPickerDialogListener,
77        AudioManager.OnAudioFocusChangeListener {
78    // STOPSHIP: Turn debugging off
79    private static final boolean DEBUG = true;
80    private static final String TAG = "TvActivity";
81
82    private static final int MSG_START_TV_RETRY = 1;
83
84    private static final int DURATION_SHOW_CHANNEL_BANNER = 2000;
85    private static final int DURATION_SHOW_CONTROL_GUIDE = 1000;
86    private static final int DURATION_SHOW_MAIN_MENU = DURATION_SHOW_CHANNEL_BANNER;
87    private static final float AUDIO_MAX_VOLUME = 1.0f;
88    private static final float AUDIO_MIN_VOLUME = 0.0f;
89    private static final float AUDIO_DUCKING_VOLUME = 0.3f;
90    private static final int START_TV_MAX_RETRY = 4;
91    private static final int START_TV_RETRY_INTERVAL = 250;
92
93    // TODO: add more KEYCODEs to the white list.
94    private static final int[] KEYCODE_WHITELIST = {
95            KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3,
96            KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7,
97            KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND,
98            KeyEvent.KEYCODE_M,
99    };
100    // TODO: this value should be able to be toggled in menu.
101    private static final boolean USE_KEYCODE_BLACKLIST = false;
102    private static final int[] KEYCODE_BLACKLIST = {
103            KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN,
104            KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT
105    };
106    // STOPSHIP: debug keys are used only for testing.
107    private static final boolean USE_DEBUG_KEYS = true;
108
109    private static final int REQUEST_START_SETUP_ACTIIVTY = 0;
110
111    private static final String LEANBACK_SET_SHYNESS_BROADCAST =
112            "com.android.mclauncher.action.SET_APP_SHYNESS";
113    private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode";
114
115    private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>();
116
117    private TvInputManager mTvInputManager;
118    private TunableTvView mTvView;
119    private LinearLayout mControlGuide;
120    private MainMenuView mMainMenuView;
121    private ChannelBannerView mChannelBanner;
122    private HideRunnable mHideChannelBanner;
123    private HideRunnable mHideControlGuide;
124    private HideRunnable mHideMainMenu;
125    private int mShortAnimationDuration;
126    private int mDisplayWidth;
127    private GestureDetector mGestureDetector;
128    private ChannelMap mChannelMap;
129    private long mInitChannelId;
130
131    private TvInput mTvInputForSetup;
132    private TvInputManagerHelper mTvInputManagerHelper;
133    private AudioManager mAudioManager;
134    private int mAudioFocusStatus;
135    private boolean mTunePendding;
136    private boolean mPipShowing;
137    private boolean mDebugNonFullSizeScreen;
138    private boolean mUseKeycodeBlacklist = USE_KEYCODE_BLACKLIST;
139    private boolean mIsShy = true;
140
141    static {
142        AVAILABLE_DIALOG_TAGS.add(InputPickerDialogFragment.DIALOG_TAG);
143        AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
144        AVAILABLE_DIALOG_TAGS.add(EditChannelsDialogFragment.DIALOG_TAG);
145        AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG);
146        AVAILABLE_DIALOG_TAGS.add(PrivacySettingDialogFragment.DIALOG_TAG);
147    }
148
149    // PIP is used for debug/verification of multiple sessions rather than real PIP feature.
150    // When PIP is enabled, the same channel as mTvView is tuned.
151    private TunableTvView mPipView;
152
153    private final Handler mHandler = new Handler() {
154        @Override
155        public void handleMessage(Message msg) {
156            if (msg.what == MSG_START_TV_RETRY) {
157                Object[] arg = (Object[]) msg.obj;
158                TvInput input = (TvInput) arg[0];
159                long channelId = (Long) arg[1];
160                int retryCount = msg.arg1;
161                startTvIfAvailableOrRetry(input, channelId, retryCount);
162            }
163        }
164    };
165
166    @Override
167    protected void onCreate(Bundle savedInstanceState) {
168        super.onCreate(savedInstanceState);
169
170        setContentView(R.layout.activity_tv);
171        mTvView = (TunableTvView) findViewById(R.id.tv_view);
172        mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() {
173            @Override
174            public boolean onUnhandledInputEvent(InputEvent event) {
175                if (event instanceof KeyEvent) {
176                    KeyEvent keyEvent = (KeyEvent) event;
177                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
178                        return onKeyUp(keyEvent.getKeyCode(), keyEvent);
179                    }
180                } else if (event instanceof MotionEvent) {
181                    MotionEvent motionEvent = (MotionEvent) event;
182                    if (motionEvent.isTouchEvent()) {
183                        return onTouchEvent(motionEvent);
184                    }
185                }
186                return false;
187            }
188        });
189        mPipView = (TunableTvView) findViewById(R.id.pip_view);
190        mPipView.setZOrderMediaOverlay(true);
191
192        mControlGuide = (LinearLayout) findViewById(R.id.control_guide);
193        mChannelBanner = (ChannelBannerView) findViewById(R.id.channel_banner);
194        mMainMenuView = (MainMenuView) findViewById(R.id.main_menu);
195        mMainMenuView.setTvActivity(this);
196
197        // Initially hide the channel banner and the control guide.
198        mChannelBanner.setVisibility(View.GONE);
199        mMainMenuView.setVisibility(View.GONE);
200        mControlGuide.setVisibility(View.GONE);
201
202        mHideControlGuide = new HideRunnable(mControlGuide, DURATION_SHOW_CONTROL_GUIDE);
203        mHideChannelBanner = new HideRunnable(mChannelBanner, DURATION_SHOW_CHANNEL_BANNER);
204        mHideMainMenu = new HideRunnable(mMainMenuView, DURATION_SHOW_MAIN_MENU);
205
206        mShortAnimationDuration = getResources().getInteger(
207                android.R.integer.config_shortAnimTime);
208
209        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
210        mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
211        Display display = getWindowManager().getDefaultDisplay();
212        Point size = new Point();
213        display.getSize(size);
214        mDisplayWidth = size.x;
215
216        mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() {
217            static final float CONTROL_MARGIN = 0.2f;
218            final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN;
219            final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN);
220
221            @Override
222            public boolean onDown(MotionEvent event) {
223                if (DEBUG) Log.d(TAG, "onDown: " + event.toString());
224                if (mChannelMap == null) {
225                    return false;
226                }
227
228                mHideControlGuide.showAndHide();
229
230                if (event.getX() <= mLeftMargin) {
231                    channelDown();
232                    return true;
233                } else if (event.getX() >= mRightMargin) {
234                    channelUp();
235                    return true;
236                }
237                return false;
238            }
239
240            @Override
241            public boolean onSingleTapUp(MotionEvent event) {
242                if (mChannelMap == null) {
243                    showInputPickerDialog();
244                    return true;
245                }
246
247                if (event.getX() > mLeftMargin && event.getX() < mRightMargin) {
248                    displayMainMenu();
249                    return true;
250                }
251                return false;
252            }
253        });
254
255        mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
256        mTvInputManagerHelper = new TvInputManagerHelper(mTvInputManager);
257        onNewIntent(getIntent());
258    }
259
260    @Override
261    protected void onNewIntent(Intent intent) {
262        // Handle the passed key press, if any. Note that only the key codes that are currently
263        // handled in the TV app will be handled via Intent.
264        // TODO: Consider defining a separate intent filter as passing data of mime type
265        // vnd.android.cursor.item/vnd.com.android.tv.channels isn't really necessary here.
266        int keyCode = intent.getIntExtra(Utils.EXTRA_KEYCODE, KeyEvent.KEYCODE_UNKNOWN);
267        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
268            if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode);
269            KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
270            onKeyUp(keyCode, event);
271            return;
272        }
273
274        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
275            // In case the channel is given explicitly, use it.
276            mInitChannelId = ContentUris.parseId(intent.getData());
277        } else {
278            mInitChannelId = Channel.INVALID_ID;
279        }
280    }
281
282    @Override
283    protected void onStart() {
284        super.onStart();
285        mTvInputManagerHelper.start();
286    }
287
288    @Override
289    protected void onResume() {
290        super.onResume();
291        mTvInputManagerHelper.update();
292        if (mTvInputManagerHelper.getTvInputSize() == 0) {
293            Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show();
294            // TODO: Direct the user to a Play Store landing page for TvInputService apps.
295            return;
296        }
297        startTv(mInitChannelId);
298        mInitChannelId = Channel.INVALID_ID;
299    }
300
301    private void startTv(long channelId) {
302        if (mTvView.isPlaying()) {
303            // TV has already started.
304            if (channelId == Channel.INVALID_ID) {
305                // Simply adjust the volume without tune.
306                setVolumeByAudioFocusStatus();
307                return;
308            }
309            Uri channelUri = mChannelMap.getCurrentChannelUri();
310            if (channelUri != null && ContentUris.parseId(channelUri) == channelId) {
311                // The requested channel is already tuned.
312                setVolumeByAudioFocusStatus();
313                return;
314            }
315            stopTv();
316        }
317
318        if (channelId == Channel.INVALID_ID) {
319            // If any initial channel id is not given, remember the last channel the user watched.
320            channelId = Utils.getLastWatchedChannelId(this);
321        }
322        if (channelId == Channel.INVALID_ID) {
323            // If failed to pick a channel, try a different input.
324            showInputPickerDialog();
325            return;
326        }
327        String inputId = Utils.getInputIdForChannel(this, channelId);
328        if (TextUtils.isEmpty(inputId)) {
329            // If failed to determine the input for that channel, try a different input.
330            showInputPickerDialog();
331            return;
332        }
333        TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(inputId);
334        if (inputInfo == null) {
335            // TODO: if the last selected TV input is uninstalled, getLastWatchedChannelId
336            // should return Channel.INVALID_ID.
337            Log.w(TAG, "Input (id=" + inputId + ") doesn't exist");
338            showInputPickerDialog();
339            return;
340        }
341        String lastSelectedInputId = Utils.getLastSelectedInputId(this);
342        TvInput input;
343        if (UnifiedTvInput.ID.equals(lastSelectedInputId)) {
344            input = new UnifiedTvInput(mTvInputManagerHelper, this);
345        } else {
346            input = new TisTvInput(mTvInputManagerHelper, inputInfo, this);
347        }
348        startTvIfAvailableOrRetry(input, channelId, 0);
349    }
350
351    private void startTvIfAvailableOrRetry(TvInput input, long channelId, int retryCount) {
352        if (!input.isAvailable()) {
353            if (retryCount >= START_TV_MAX_RETRY) {
354                showInputPickerDialog();
355                return;
356            }
357            if (DEBUG) Log.d(TAG, "Retry start TV (retryCount=" + retryCount + ")");
358            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TV_RETRY,
359                    retryCount + 1, 0, new Object[]{input, channelId}),
360                    START_TV_RETRY_INTERVAL);
361            return;
362        }
363        startTv(input, channelId);
364    }
365
366    @Override
367    protected void onStop() {
368        if (DEBUG) Log.d(TAG, "onStop()");
369        mHandler.removeMessages(MSG_START_TV_RETRY);
370        stopTv();
371        stopPip();
372        if (!isShyModeSet()) {
373            setShynessMode(true);
374        }
375        mTvInputManagerHelper.stop();
376        super.onStop();
377    }
378
379    @Override
380    public void onInputPicked(TvInput input) {
381        if (input.equals(getSelectedTvInput())) {
382            // Nothing has changed thus nothing to do.
383            return;
384        }
385        if (!input.hasChannel(false)) {
386            mTvInputForSetup = null;
387            if (input.hasActivity(Utils.ACTION_SETUP)) {
388                startSetupActivity(input);
389            } else {
390                Toast.makeText(this, R.string.empty_channel_tvinput, Toast.LENGTH_SHORT).show();
391                showInputPickerDialog();
392            }
393            return;
394        }
395
396        stopTv();
397        startTvWithLastWatchedChannel(input);
398    }
399
400    public TvInputManagerHelper getTvInputManagerHelper() {
401        return mTvInputManagerHelper;
402    }
403
404    public TvInput getSelectedTvInput() {
405        return mChannelMap == null ? null : mChannelMap.getTvInput();
406    }
407
408    public void showEditChannelsDialog() {
409        if (getSelectedTvInput() == null) {
410            return;
411        }
412
413        showDialogFragment(EditChannelsDialogFragment.DIALOG_TAG, new EditChannelsDialogFragment());
414    }
415
416    public void showInputPickerDialog() {
417        showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, new InputPickerDialogFragment());
418    }
419
420    public void startSettingsActivity() {
421        if (getSelectedTvInput() == null) {
422            Log.w(TAG, "There is no selected TV input during startSettingsActivity");
423            return;
424        }
425        getSelectedTvInput().startActivity(Utils.ACTION_SETTINGS);
426    }
427
428    public void startSetupActivity() {
429        if (getSelectedTvInput() != null) {
430            startSetupActivity(getSelectedTvInput());
431        }
432    }
433
434    public void startSetupActivity(TvInput input) {
435        if (input.startActivityForResult(this, Utils.ACTION_SETUP, REQUEST_START_SETUP_ACTIIVTY)) {
436            mTvInputForSetup = input;
437            stopTv();
438        } else {
439            String displayName = input.getDisplayName();
440            String message = String.format(getString(
441                    R.string.input_setup_activity_not_found), displayName);
442            new AlertDialog.Builder(this)
443                    .setMessage(message)
444                    .setPositiveButton(R.string.OK, null)
445                    .show();
446        }
447    }
448
449    public void showAspectRatioOption() {
450        AspectRatioOptionFragment f = new AspectRatioOptionFragment();
451        FragmentTransaction ft = getFragmentManager().beginTransaction();
452        ft.add(R.id.right_panel, f);
453        ft.addToBackStack(null);
454        // TODO: add an animation.
455        ft.commit();
456    }
457
458    public void showClosedCaptionOption() {
459        ClosedCaptionOptionFragment f = new ClosedCaptionOptionFragment();
460        FragmentTransaction ft = getFragmentManager().beginTransaction();
461        ft.add(R.id.right_panel, f);
462        ft.addToBackStack(null);
463        // TODO: add an animation.
464        ft.commit();
465    }
466
467    @Override
468    public void onActivityResult(int requestCode, int resultCode, Intent data) {
469        switch (requestCode) {
470            case REQUEST_START_SETUP_ACTIIVTY:
471                if (resultCode == Activity.RESULT_OK && mTvInputForSetup != null) {
472                    startTvWithLastWatchedChannel(mTvInputForSetup);
473                }
474                break;
475
476            default:
477                //TODO: Handle failure of setup.
478        }
479        mTvInputForSetup = null;
480    }
481
482    @Override
483    public boolean dispatchKeyEvent(KeyEvent event) {
484        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
485        int eventKeyCode = event.getKeyCode();
486        if (mUseKeycodeBlacklist) {
487            for (int keycode : KEYCODE_BLACKLIST) {
488                if (keycode == eventKeyCode) {
489                    return super.dispatchKeyEvent(event);
490                }
491            }
492            return dispatchKeyEventToSession(event);
493        } else {
494            for (int keycode : KEYCODE_WHITELIST) {
495                if (keycode == eventKeyCode) {
496                    return dispatchKeyEventToSession(event);
497                }
498            }
499            return super.dispatchKeyEvent(event);
500        }
501    }
502
503    @Override
504    public void onAudioFocusChange(int focusChange) {
505        mAudioFocusStatus = focusChange;
506        setVolumeByAudioFocusStatus();
507    }
508
509    private void setVolumeByAudioFocusStatus() {
510        if (mTvView.isPlaying()) {
511            switch (mAudioFocusStatus) {
512                case AudioManager.AUDIOFOCUS_GAIN:
513                    mTvView.setVolume(AUDIO_MAX_VOLUME);
514                    break;
515                case AudioManager.AUDIOFOCUS_LOSS:
516                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
517                    mTvView.setVolume(AUDIO_MIN_VOLUME);
518                    break;
519                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
520                    mTvView.setVolume(AUDIO_DUCKING_VOLUME);
521                    break;
522            }
523        }
524    }
525
526    private void startTvWithLastWatchedChannel(TvInput input) {
527        long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId());
528        startTv(input, channelId);
529    }
530
531    private void startTv(TvInput input, long channelId) {
532        if (mChannelMap != null) {
533            // TODO: when this case occurs, we should remove the case.
534            Log.w(TAG, "The previous variables are not released in startTv");
535            stopTv();
536        }
537
538        mMainMenuView.setChannelMap(null);
539        int result = mAudioManager.requestAudioFocus(TvActivity.this,
540                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
541        mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
542                        AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
543
544        // Prepare a new channel map for the current input.
545        mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished);
546        mTvView.start(mTvInputManagerHelper);
547        setVolumeByAudioFocusStatus();
548        tune();
549    }
550
551    private void stopTv() {
552        if (mTvView.isPlaying()) {
553            mTvView.stop();
554            mAudioManager.abandonAudioFocus(this);
555        }
556        if (mChannelMap != null) {
557            mMainMenuView.setChannelMap(null);
558            mChannelMap.close();
559            mChannelMap = null;
560        }
561        mTunePendding = false;
562    }
563
564    private boolean isPlaying() {
565        return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID;
566    }
567
568    private void startPip() {
569        if (!isPlaying()) {
570            Log.w(TAG, "TV content should be playing");
571            return;
572        }
573        if (DEBUG) Log.d(TAG, "startPip()");
574        mPipView.start(mTvInputManagerHelper);
575        boolean success = mPipView.tuneTo(mTvView.getCurrentChannelId(), new OnTuneListener() {
576            @Override
577            public void onUnexpectedStop(long channelId) {
578                Log.w(TAG, "The PIP is Unexpectedly stopped");
579                stopPip();
580            }
581
582            @Override
583            public void onTuned(boolean success, long channelId) {
584                if (!success) {
585                    Log.w(TAG, "Fail to start the PIP during channel tunning");
586                    stopPip();
587                } else {
588                    mPipView.setVisibility(View.VISIBLE);
589                }
590            }
591
592            @Override
593            public void onStreamInfoChanged(StreamInfo info) {
594                // Do nothing.
595            }
596        });
597        if (!success) {
598            Log.w(TAG, "Fail to start the PIP");
599            return;
600        }
601        mPipView.setVolume(AUDIO_MIN_VOLUME);
602        mPipShowing = true;
603    }
604
605    private void stopPip() {
606        if (DEBUG) Log.d(TAG, "stopPip");
607        if (mPipView.isPlaying()) {
608            mPipView.setVisibility(View.INVISIBLE);
609            mPipView.stop();
610        }
611        mPipShowing = false;
612    }
613
614    private final Runnable mOnChannelsLoadFinished = new Runnable() {
615        @Override
616        public void run() {
617            if (mTunePendding) {
618                tune();
619            }
620            mMainMenuView.setChannelMap(mChannelMap);
621        }
622    };
623
624    private void tune() {
625        if (DEBUG) Log.d(TAG, "tune()");
626        // Prerequisites to be able to tune.
627        if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
628            if (DEBUG) Log.d(TAG, "Channel map not ready");
629            mTunePendding = true;
630            return;
631        }
632        mTunePendding = false;
633        long channelId = mChannelMap.getCurrentChannelId();
634        final String inputId = mChannelMap.getTvInput().getId();
635        if (channelId == Channel.INVALID_ID) {
636            stopTv();
637            Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
638            return;
639        }
640
641        mTvView.tuneTo(channelId, new OnTuneListener() {
642            @Override
643            public void onUnexpectedStop(long channelId) {
644                stopTv();
645                startTv(Channel.INVALID_ID);
646            }
647
648            @Override
649            public void onTuned(boolean success, long channelId) {
650                if (!success) {
651                    Log.w(TAG, "Failed to tune to channel " + channelId);
652                    // TODO: show something to user about this error.
653                } else {
654                    Utils.setLastWatchedChannelId(TvActivity.this, inputId,
655                            channelId);
656                }
657            }
658
659            @Override
660            public void onStreamInfoChanged(StreamInfo info) {
661                updateChannelBanner(false);
662            }
663        });
664        updateChannelBanner(true);
665        if (isShyModeSet()) {
666            setShynessMode(false);
667            // TODO: Set the shy mode to true when tune() fails.
668        }
669    }
670
671    private void updateChannelBanner(final boolean showBanner) {
672        runOnUiThread(new Runnable() {
673            @Override
674            public void run() {
675                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
676                    return;
677                }
678
679                mChannelBanner.updateViews(mChannelMap, mTvView);
680                if (showBanner) {
681                    mHideChannelBanner.showAndHide();
682                }
683            }
684        });
685    }
686
687    private void displayMainMenu() {
688        runOnUiThread(new Runnable() {
689            @Override
690            public void run() {
691                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
692                    return;
693                }
694
695                mHideMainMenu.showAndHide();
696            }
697        });
698    }
699
700    public void showRecentlyWatchedDialog() {
701        showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
702                new RecentlyWatchedDialogFragment());
703    }
704
705    @Override
706    protected void onSaveInstanceState(Bundle outState) {
707        // Do not save instance state because restoring instance state when TV app died
708        // unexpectedly can cause some problems like initializing fragments duplicately and
709        // accessing resource before it is initialzed.
710    }
711
712    @Override
713    protected void onDestroy() {
714        if (DEBUG) Log.d(TAG, "onDestroy()");
715        super.onDestroy();
716    }
717
718    @Override
719    public boolean onKeyUp(int keyCode, KeyEvent event) {
720        if (getFragmentManager().getBackStackEntryCount() > 0) {
721            if (keyCode == KeyEvent.KEYCODE_BACK) {
722                getFragmentManager().popBackStack();
723                return true;
724            }
725            return super.onKeyUp(keyCode, event);
726        }
727        if (mMainMenuView.getVisibility() == View.VISIBLE) {
728            if (keyCode == KeyEvent.KEYCODE_BACK) {
729                mMainMenuView.setVisibility(View.GONE);
730                return true;
731            }
732            return super.onKeyUp(keyCode, event);
733        }
734
735        if (mHandler.hasMessages(MSG_START_TV_RETRY)) {
736            // Ignore key events during startTv retry.
737            return true;
738        }
739        if (mChannelMap == null) {
740            switch (keyCode) {
741                case KeyEvent.KEYCODE_H:
742                    showRecentlyWatchedDialog();
743                    return true;
744                case KeyEvent.KEYCODE_TV_INPUT:
745                case KeyEvent.KEYCODE_I:
746                case KeyEvent.KEYCODE_CHANNEL_UP:
747                case KeyEvent.KEYCODE_DPAD_UP:
748                case KeyEvent.KEYCODE_CHANNEL_DOWN:
749                case KeyEvent.KEYCODE_DPAD_DOWN:
750                case KeyEvent.KEYCODE_NUMPAD_ENTER:
751                case KeyEvent.KEYCODE_DPAD_CENTER:
752                case KeyEvent.KEYCODE_E:
753                case KeyEvent.KEYCODE_MENU:
754                    showInputPickerDialog();
755                    return true;
756            }
757        } else {
758            switch (keyCode) {
759                case KeyEvent.KEYCODE_H:
760                    showRecentlyWatchedDialog();
761                    return true;
762
763                case KeyEvent.KEYCODE_TV_INPUT:
764                case KeyEvent.KEYCODE_I:
765                    showInputPickerDialog();
766                    return true;
767
768                case KeyEvent.KEYCODE_CHANNEL_UP:
769                case KeyEvent.KEYCODE_DPAD_UP:
770                    channelUp();
771                    return true;
772
773                case KeyEvent.KEYCODE_CHANNEL_DOWN:
774                case KeyEvent.KEYCODE_DPAD_DOWN:
775                    channelDown();
776                    return true;
777
778                case KeyEvent.KEYCODE_DPAD_LEFT:
779                case KeyEvent.KEYCODE_DPAD_RIGHT:
780                    displayMainMenu();
781                    return true;
782
783                case KeyEvent.KEYCODE_ENTER:
784                case KeyEvent.KEYCODE_NUMPAD_ENTER:
785                case KeyEvent.KEYCODE_E:
786                case KeyEvent.KEYCODE_DPAD_CENTER:
787                case KeyEvent.KEYCODE_MENU:
788                    if (event.isCanceled()) {
789                        return true;
790                    }
791                    if (keyCode != KeyEvent.KEYCODE_MENU) {
792                        updateChannelBanner(true);
793                    }
794                    if (keyCode != KeyEvent.KEYCODE_E) {
795                        displayMainMenu();
796                    }
797                    return true;
798            }
799        }
800        if (USE_DEBUG_KEYS) {
801            switch (keyCode) {
802                case KeyEvent.KEYCODE_W: {
803                    mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
804                    if (mDebugNonFullSizeScreen) {
805                        mTvView.layout(100, 100, 400, 300);
806                    } else {
807                        ViewGroup.LayoutParams params = mTvView.getLayoutParams();
808                        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
809                        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
810                        mTvView.setLayoutParams(params);
811                    }
812                    return true;
813                }
814                case KeyEvent.KEYCODE_P: {
815                    togglePipView();
816                    return true;
817                }
818                case KeyEvent.KEYCODE_CTRL_LEFT:
819                case KeyEvent.KEYCODE_CTRL_RIGHT: {
820                    mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
821                    return true;
822                }
823                case KeyEvent.KEYCODE_O: {
824                    showAspectRatioOption();
825                    return true;
826                }
827            }
828        }
829        return super.onKeyUp(keyCode, event);
830    }
831
832    @Override
833    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
834        if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event);
835        // Treat the BACK key long press as the normal press since we changed the behavior in
836        // onBackPressed().
837        if (keyCode == KeyEvent.KEYCODE_BACK) {
838            super.onBackPressed();
839            return true;
840        }
841        return false;
842    }
843
844    @Override
845    public void onBackPressed() {
846        if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) {
847            // TODO: show the following toast message in the future.
848//            Toast.makeText(getApplicationContext(), getResources().getString(
849//                    R.string.long_press_back), Toast.LENGTH_SHORT).show();
850
851            // If back key would exit TV app,
852            // show McLauncher instead so we can get benefit of McLauncher's shyMode.
853            Intent startMain = new Intent(Intent.ACTION_MAIN);
854            startMain.addCategory(Intent.CATEGORY_HOME);
855            startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
856            startActivity(startMain);
857        } else {
858            super.onBackPressed();
859        }
860    }
861
862    @Override
863    public void onUserInteraction() {
864        super.onUserInteraction();
865        if (mMainMenuView.getVisibility() == View.VISIBLE) {
866            mHideMainMenu.showAndHide();
867        }
868    }
869
870    @Override
871    public boolean onTouchEvent(MotionEvent event) {
872        if (mMainMenuView.getVisibility() != View.VISIBLE) {
873            mGestureDetector.onTouchEvent(event);
874        }
875        return super.onTouchEvent(event);
876    }
877
878    public void togglePipView() {
879        if (mPipShowing) {
880            stopPip();
881        } else {
882            startPip();
883        }
884    }
885
886    private boolean dispatchKeyEventToSession(final KeyEvent event) {
887        if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
888        if (mTvView != null) {
889            return mTvView.dispatchKeyEvent(event);
890        }
891        return false;
892    }
893
894    public void moveToChannel(long id) {
895        if (mChannelMap != null && mChannelMap.isLoadFinished()
896                && id != mChannelMap.getCurrentChannelId()) {
897            if (mChannelMap.moveToChannel(id)) {
898                tune();
899            } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) {
900                startTv(id);
901            } else {
902                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
903            }
904        }
905    }
906
907    private void channelUp() {
908        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
909            if (mChannelMap.moveToNextChannel()) {
910                tune();
911            } else {
912                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
913            }
914        }
915    }
916
917    private void channelDown() {
918        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
919            if (mChannelMap.moveToPreviousChannel()) {
920                tune();
921            } else {
922                Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show();
923            }
924        }
925    }
926
927    public void showDialogFragment(final String tag, final DialogFragment dialog) {
928        // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV.
929        if (!AVAILABLE_DIALOG_TAGS.contains(tag)) {
930            return;
931        }
932        mHandler.post(new Runnable() {
933            @Override
934            public void run() {
935                FragmentManager fm = getFragmentManager();
936                fm.executePendingTransactions();
937
938                for (String availableTag : AVAILABLE_DIALOG_TAGS) {
939                    if (fm.findFragmentByTag(availableTag) != null) {
940                        return;
941                    }
942                }
943
944                FragmentTransaction ft = getFragmentManager().beginTransaction();
945                ft.addToBackStack(null);
946                dialog.show(ft, tag);
947            }
948        });
949    }
950
951    private class HideRunnable implements Runnable {
952        private final View mView;
953        private final long mWaitingTime;
954        private boolean mOnHideAnimation;
955
956        public HideRunnable(View view, long waitingTime) {
957            mView = view;
958            mWaitingTime = waitingTime;
959        }
960
961        @Override
962        public void run() {
963            mOnHideAnimation = true;
964            mView.animate()
965                    .alpha(0f)
966                    .setDuration(mShortAnimationDuration)
967                    .setListener(new AnimatorListenerAdapter() {
968                        @Override
969                        public void onAnimationEnd(Animator animation) {
970                            mOnHideAnimation = false;
971                            mView.setVisibility(View.GONE);
972                        }
973                    });
974        }
975
976        private void showAndHide() {
977            if (mView.getVisibility() != View.VISIBLE) {
978                mView.setAlpha(0f);
979                mView.setVisibility(View.VISIBLE);
980                mView.animate()
981                        .alpha(1f)
982                        .setDuration(mShortAnimationDuration)
983                        .setListener(new AnimatorListenerAdapter() {
984                            @Override
985                            public void onAnimationEnd(Animator animation) {
986                                // Currently the target alpha isn't kept, but it was before.
987                                // TODO: Remove this if frameworks keeps the target value again.
988                                mView.setAlpha(1f);
989                            }
990                        });
991            }
992            // Schedule the hide animation after a few seconds.
993            mHandler.removeCallbacks(this);
994            if (mOnHideAnimation) {
995                mView.clearAnimation();
996                mView.setAlpha(1f);
997                mOnHideAnimation = false;
998            }
999            mHandler.postDelayed(this, mWaitingTime);
1000        }
1001    }
1002
1003    private void setShynessMode(boolean shyMode) {
1004        mIsShy = shyMode;
1005        Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST);
1006        intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode);
1007        sendBroadcast(intent);
1008    }
1009
1010    private boolean isShyModeSet() {
1011        return mIsShy;
1012    }
1013}
1014