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