TvActivity.java revision e7236b5a46375618f553c3b54a90c89cf77088ab
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.Fragment;
25import android.app.FragmentTransaction;
26import android.content.ComponentName;
27import android.content.ContentUris;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.database.ContentObserver;
33import android.graphics.Point;
34import android.media.AudioManager;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.provider.TvContract;
39import android.tv.TvInputInfo;
40import android.tv.TvInputManager;
41import android.tv.TvInputService;
42import android.tv.TvView;
43import android.util.Log;
44import android.view.Display;
45import android.view.GestureDetector;
46import android.view.InputEvent;
47import android.view.KeyEvent;
48import android.view.MotionEvent;
49import android.view.SurfaceHolder;
50import android.view.View;
51import android.view.ViewGroup;
52import android.widget.LinearLayout;
53import android.widget.TextView;
54import android.widget.Toast;
55
56import com.android.tv.menu.MenuDialogFragment;
57
58import java.util.List;
59
60/**
61 * The main activity for demonstrating TV app.
62 */
63public class TvActivity extends Activity implements
64        InputPickerDialogFragment.InputPickerDialogListener,
65        AudioManager.OnAudioFocusChangeListener {
66    // STOPSHIP: Turn debugging off
67    private static final boolean DEBUG = true;
68    private static final String TAG = "TvActivity";
69
70    private static final int DURATION_SHOW_CHANNEL_BANNER = 2000;
71    private static final int DURATION_SHOW_CONTROL_GUIDE = 1000;
72    private static final float AUDIO_MAX_VOLUME = 1.0f;
73    private static final float AUDIO_MIN_VOLUME = 0.0f;
74    private static final float AUDIO_DUCKING_VOLUME = 0.3f;
75    private static final int DELAY_FOR_SURFACE_RELEASE = 300;
76    // TODO: add more KEYCODEs to the white list.
77    private static final int[] KEYCODE_WHITELIST = {
78            KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3,
79            KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_7,
80            KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND,
81            KeyEvent.KEYCODE_M,
82    };
83    // TODO: this value should be able to be toggled in menu.
84    private static final boolean USE_KEYCODE_BLACKLIST = false;
85    private static final int[] KEYCODE_BLACKLIST = {
86            KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN,
87            KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT
88    };
89    // STOPSHIP: debug keys are used only for testing.
90    private static final boolean USE_DEBUG_KEYS = true;
91
92    private static final int REQUEST_START_SETUP_ACTIIVTY = 0;
93
94    private static final String LEANBACK_SET_SHYNESS_BROADCAST =
95            "com.android.mclauncher.action.SET_APP_SHYNESS";
96    private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode";
97
98    private TvInputManager mTvInputManager;
99    private TvView mTvView;
100    private LinearLayout mControlGuide;
101    private LinearLayout mChannelBanner;
102    private Runnable mHideChannelBanner;
103    private Runnable mHideControlGuide;
104    private TextView mChannelTextView;
105    private TextView mProgramTextView;
106    private int mShortAnimationDuration;
107    private int mDisplayWidth;
108    private GestureDetector mGestureDetector;
109    private ChannelMap mChannelMap;
110    private TvInputManager.Session mTvSession;
111    private TvInputInfo mTvInputInfo;
112    private TvInputInfo mTvInputInfoForSetup;
113    private AudioManager mAudioManager;
114    private int mAudioFocusStatus;
115    private final Handler mHandler = new Handler();
116    private boolean mDefaultSessionRequested;
117    private boolean mTunePendding;
118    private boolean mPipShowing;
119    private boolean mDebugNonFullSizeScreen;
120    private boolean mUseKeycodeBlacklist = USE_KEYCODE_BLACKLIST;
121    private boolean mIsShy = true;
122
123    private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
124        @Override
125        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { }
126
127        @Override
128        public void surfaceCreated(SurfaceHolder holder) { }
129
130        @Override
131        public void surfaceDestroyed(SurfaceHolder holder) {
132            // TODO: It is a hack to wait to release a surface at TIS. If there is a way to
133            // know when the surface is released at TIS, we don't need this hack.
134            try {
135                if (DEBUG) Log.d(TAG, "Sleep to wait destroying a surface");
136                Thread.sleep(DELAY_FOR_SURFACE_RELEASE);
137                if (DEBUG) Log.d(TAG, "Wake up from sleeping");
138            } catch (InterruptedException e) {
139                e.printStackTrace();
140            }
141        }
142    };
143
144    // PIP is used for debug/verification of multiple sessions rather than real PIP feature.
145    // When PIP is enabled, the same channel as mTvView is tuned.
146    private TvView mPipView;
147    private TvInputManager.Session mPipSession;
148    private TvInputInfo mPipInputInfo;
149
150    @Override
151    protected void onCreate(Bundle savedInstanceState) {
152        super.onCreate(savedInstanceState);
153
154        setContentView(R.layout.activity_tv);
155        mTvView = (TvView) findViewById(R.id.tv_view);
156        mTvView.setOnUnhandledInputEventListener(new TvView.OnUnhandledInputEventListener() {
157            @Override
158            public boolean onUnhandledInputEvent(InputEvent event) {
159                if (event instanceof KeyEvent) {
160                    KeyEvent keyEvent = (KeyEvent) event;
161                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
162                        return onKeyUp(keyEvent.getKeyCode(), keyEvent);
163                    }
164                } else if (event instanceof MotionEvent) {
165                    MotionEvent motionEvent = (MotionEvent) event;
166                    if (motionEvent.isTouchEvent()) {
167                        return onTouchEvent(motionEvent);
168                    }
169                }
170                return false;
171            }
172        });
173        mTvView.getHolder().addCallback(mSurfaceHolderCallback);
174        mPipView = (TvView) findViewById(R.id.pip_view);
175        mPipView.setZOrderMediaOverlay(true);
176        mPipView.getHolder().addCallback(mSurfaceHolderCallback);
177
178        mControlGuide = (LinearLayout) findViewById(R.id.control_guide);
179        mChannelBanner = (LinearLayout) findViewById(R.id.channel_banner);
180
181        // Initially hide the channel banner and the control guide.
182        mChannelBanner.setVisibility(View.GONE);
183        mControlGuide.setVisibility(View.GONE);
184
185        mHideControlGuide = new HideRunnable(mControlGuide);
186        mHideChannelBanner = new HideRunnable(mChannelBanner);
187
188        mChannelTextView = (TextView) findViewById(R.id.channel_text);
189        mProgramTextView = (TextView) findViewById(R.id.program_text);
190
191        mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
192
193        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
194        mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
195        Display display = getWindowManager().getDefaultDisplay();
196        Point size = new Point();
197        display.getSize(size);
198        mDisplayWidth = size.x;
199
200        mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
201            static final float CONTROL_MARGIN = 0.2f;
202            final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN;
203            final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN);
204
205            @Override
206            public boolean onDown(MotionEvent event) {
207                Log.d(TAG, "onDown: " + event.toString());
208                if (mChannelMap == null) {
209                    return false;
210                }
211
212                showAndHide(mControlGuide, mHideControlGuide, DURATION_SHOW_CONTROL_GUIDE);
213
214                if (event.getX() <= mLeftMargin) {
215                    channelDown();
216                    return true;
217                } else if (event.getX() >= mRightMargin) {
218                    channelUp();
219                    return true;
220                }
221                return false;
222            }
223
224            @Override
225            public boolean onSingleTapUp(MotionEvent event) {
226                if (mChannelMap == null) {
227                    showInputPickerDialog();
228                    return true;
229                }
230
231                if (event.getX() > mLeftMargin && event.getX() < mRightMargin) {
232                    showMenu();
233                    return true;
234                }
235                return false;
236            }
237        });
238
239        getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, true,
240                mProgramUpdateObserver);
241
242        mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
243    }
244
245    @Override
246    protected void onNewIntent(Intent intent) {
247        setIntent(intent);
248    }
249
250    @Override
251    protected void onResume() {
252        super.onResume();
253        startDefaultSession();
254    }
255
256    private void startDefaultSession() {
257        if (mTvInputManager == null) {
258            return;
259        }
260        // TODO: Remove this check after TvInputManagerService becomes system service.
261        if (mDefaultSessionRequested) {
262            return;
263        }
264        mDefaultSessionRequested = true;
265        if (mTunePendding) {
266            return;
267        }
268
269        // Check whether the system has at least one TvInputService app installed.
270        final List<ResolveInfo> services = getPackageManager().queryIntentServices(
271                new Intent(TvInputService.SERVICE_INTERFACE), PackageManager.GET_SERVICES);
272        if (services == null || services.isEmpty()) {
273            Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show();
274            // TODO: Direct the user to a Play Store landing page for TvInputService apps.
275            return;
276        }
277
278        // Figure out the initial channel to tune to.
279        long channelId = Channel.INVALID_ID;
280        Intent intent = getIntent();
281        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
282            // In case the channel is given explicitly, use it.
283            channelId = ContentUris.parseId(intent.getData());
284        }
285        if (channelId == Channel.INVALID_ID) {
286            // Otherwise, remember the last channel the user watched.
287            channelId = TvInputUtils.getLastWatchedChannelId(this);
288        }
289        if (channelId == Channel.INVALID_ID) {
290            // If failed to pick a channel, try a different input.
291            showInputPickerDialog();
292            return;
293        }
294        ComponentName inputName = TvInputUtils.getInputNameForChannel(this, channelId);
295        if (inputName == null) {
296            // If failed to determine the input for that channel, try a different input.
297            showInputPickerDialog();
298            return;
299        }
300        // If the session is already started, simply adjust the volume without tune.
301        if (mTvSession != null) {
302            setVolumeByAudioFocusStatus();
303        } else {
304            List<TvInputInfo> inputList = mTvInputManager.getTvInputList();
305            for (TvInputInfo info : inputList) {
306                if (inputName.equals(info.getComponent())) {
307                    startSession(info, channelId);
308                    break;
309                }
310            }
311        }
312    }
313
314    @Override
315    protected void onStop() {
316        if (DEBUG) Log.d(TAG, "onStop() -- stop all sessions");
317        stopSession();
318        stopPipSession();
319        mDefaultSessionRequested = false;
320        super.onStop();
321    }
322
323    public void showInputPickerDialog() {
324        showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, new InputPickerDialogFragment());
325    }
326
327    @Override
328    public void onInputPicked(final TvInputInfo selectedTvInput, final String displayName) {
329        if (mTvSession != null && selectedTvInput.equals(mTvInputInfo)) {
330            // Nothing has changed thus nothing to do.
331            return;
332        }
333
334        if (!TvInputUtils.hasChannel(this, selectedTvInput)) {
335            mTvInputInfoForSetup = null;
336            if (showSetupActivity(selectedTvInput, displayName)) {
337                stopSession();
338            }
339            return;
340        }
341
342        // Start a new session with the new input.
343        stopSession();
344
345        // TODO: It is a hack to wait to release a surface at TIS. If there is a way to
346        // know when the surface is released at TIS, we don't need this hack.
347        mHandler.postDelayed(new Runnable() {
348            @Override
349            public void run() {
350                startSession(selectedTvInput);
351            }
352        }, DELAY_FOR_SURFACE_RELEASE);
353    }
354
355    private boolean showSetupActivity(TvInputInfo inputInfo, String displayName) {
356        PackageManager pm = getPackageManager();
357        List<ResolveInfo> activityInfos = pm.queryIntentActivities(
358                new Intent(TvInputUtils.ACTION_SETUP), PackageManager.GET_ACTIVITIES);
359        ResolveInfo setupActivity = null;
360        if (activityInfos != null) {
361            for (ResolveInfo info : activityInfos) {
362                if (info.activityInfo.packageName.equals(inputInfo.getPackageName())) {
363                    setupActivity = info;
364                }
365            }
366        }
367
368        if (setupActivity == null) {
369            String message = String.format(getString(R.string.input_setup_activity_not_found),
370                    displayName);
371            new AlertDialog.Builder(this)
372                    .setMessage(message)
373                    .setPositiveButton(R.string.OK, null)
374                    .show();
375            return false;
376        }
377
378        mTvInputInfoForSetup = inputInfo;
379        Intent intent = new Intent(TvInputUtils.ACTION_SETUP);
380        intent.setClassName(setupActivity.activityInfo.packageName,
381                setupActivity.activityInfo.name);
382        startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY);
383
384        return true;
385    }
386
387    @Override
388    public void onActivityResult(int requestCode, int resultCode, Intent data) {
389        switch (requestCode) {
390            case REQUEST_START_SETUP_ACTIIVTY:
391                if (resultCode == Activity.RESULT_OK && mTvInputInfoForSetup != null) {
392                    startSession(mTvInputInfoForSetup);
393                }
394                break;
395
396            default:
397                //TODO: Handle failure of setup.
398        }
399        mTvInputInfoForSetup = null;
400    }
401
402    @Override
403    public boolean dispatchKeyEvent(KeyEvent event) {
404        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
405        int eventKeyCode = event.getKeyCode();
406        if (mUseKeycodeBlacklist) {
407            for (int keycode : KEYCODE_BLACKLIST) {
408                if (keycode == eventKeyCode) {
409                    return super.dispatchKeyEvent(event);
410                }
411            }
412            return dispatchKeyEventToSession(event);
413        } else {
414            for (int keycode : KEYCODE_WHITELIST) {
415                if (keycode == eventKeyCode) {
416                    return dispatchKeyEventToSession(event);
417                }
418            }
419            return super.dispatchKeyEvent(event);
420        }
421    }
422
423    @Override
424    public void onAudioFocusChange(int focusChange) {
425        mAudioFocusStatus = focusChange;
426        setVolumeByAudioFocusStatus();
427    }
428
429    private void setVolumeByAudioFocusStatus() {
430        if (mTvSession != null) {
431            switch (mAudioFocusStatus) {
432                case AudioManager.AUDIOFOCUS_GAIN:
433                    mTvSession.setVolume(AUDIO_MAX_VOLUME);
434                    break;
435                case AudioManager.AUDIOFOCUS_LOSS:
436                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
437                    mTvSession.setVolume(AUDIO_MIN_VOLUME);
438                    break;
439                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
440                    mTvSession.setVolume(AUDIO_DUCKING_VOLUME);
441                    break;
442            }
443        }
444    }
445
446    private void startSession(TvInputInfo selectedTvInput) {
447        long channelId = TvInputUtils.getLastWatchedChannelId(TvActivity.this,
448                selectedTvInput.getId());
449        startSession(selectedTvInput, channelId);
450    }
451
452    private void startSession(TvInputInfo inputInfo, long channelId) {
453        // TODO: recreate SurfaceView to prevent abusing from the previous session.
454        mTvInputInfo = inputInfo;
455        // Prepare a new channel map for the current input.
456        mChannelMap = new ChannelMap(this, inputInfo.getComponent(), channelId,
457                mOnChannelsLoadFinished);
458        // Create a new session and start.
459        mTvView.bindTvInput(inputInfo.getComponent(), mSessionCreated);
460        tune();
461    }
462
463    private final TvInputManager.SessionCreateCallback mSessionCreated =
464            new TvInputManager.SessionCreateCallback() {
465                @Override
466                public void onSessionCreated(TvInputManager.Session session) {
467                    if (session != null) {
468                        mTvSession = session;
469                        int result = mAudioManager.requestAudioFocus(TvActivity.this,
470                                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
471                        mAudioFocusStatus =
472                                (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
473                                        AudioManager.AUDIOFOCUS_GAIN
474                                        : AudioManager.AUDIOFOCUS_LOSS;
475                        if (mTunePendding) {
476                            tune();
477                        }
478                    } else {
479                        Log.w(TAG, "Failed to create a session");
480                        // TODO: show something to user about this error.
481                    }
482                }
483            };
484
485    private void startPipSession() {
486        if (mTvSession == null) {
487            Log.w(TAG, "TV content should be playing.");
488            return;
489        }
490        Log.d(TAG, "startPipSession");
491        mPipInputInfo = mTvInputInfo;
492        mPipView.bindTvInput(mPipInputInfo.getComponent(), mPipSessionCreated);
493        mPipShowing = true;
494    }
495
496    private final TvInputManager.SessionCreateCallback mPipSessionCreated =
497            new TvInputManager.SessionCreateCallback() {
498                @Override
499                public void onSessionCreated(final TvInputManager.Session session) {
500                    Log.d(TAG, "PIP session is created.");
501                    if (mTvSession == null) {
502                        Log.w(TAG, "TV content should be playing.");
503                        if (session != null) {
504                            mPipView.unbindTvInput();
505                        }
506                        mPipShowing = false;
507                        return;
508                    }
509                    if (session == null) {
510                        Log.w(TAG, "Fail to create another session.");
511                        mPipShowing = false;
512                        return;
513                    }
514                    runOnUiThread(new Runnable() {
515                        @Override
516                        public void run() {
517                            mPipSession = session;
518                            mPipSession.setVolume(0);
519                            mPipSession.tune(mChannelMap.getCurrentChannelUri());
520                            mPipView.setVisibility(View.VISIBLE);
521                        }
522                    });
523                }
524            };
525
526    private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) {
527        @Override
528        public void onChange(boolean selfChange, Uri uri) {
529            if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
530                return;
531            }
532            Uri channelUri = mChannelMap.getCurrentChannelUri();
533            if (channelUri == null) {
534                return;
535            }
536            Program program = TvInputUtils.getCurrentProgram(TvActivity.this, channelUri);
537            if (program == null) {
538                return;
539            }
540            mProgramTextView.setText(program.getTitle());
541        }
542    };
543
544    private final Runnable mOnChannelsLoadFinished = new Runnable() {
545        @Override
546        public void run() {
547            if (mTunePendding) {
548                tune();
549            }
550        }
551    };
552
553    private void tune() {
554        Log.d(TAG, "tune()");
555        // Prerequisites to be able to tune.
556        if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
557            Log.d(TAG, "Channel map not ready");
558            mTunePendding = true;
559            return;
560        }
561        if (mTvSession == null) {
562            Log.d(TAG, "Service not connected");
563            mTunePendding = true;
564            return;
565        }
566        setVolumeByAudioFocusStatus();
567
568        Uri currentChannelUri = mChannelMap.getCurrentChannelUri();
569        if (currentChannelUri != null) {
570            // TODO: implement 'no signal'
571            // TODO: add result callback and show a message on failure.
572            TvInputUtils.setLastWatchedChannel(this, mTvInputInfo.getId(), currentChannelUri);
573            mTvSession.tune(currentChannelUri);
574            if (isShyModeSet()) {
575                setShynessMode(false);
576                // TODO: Set the shy mode to true when tune() fails.
577            }
578            displayChannelBanner();
579        }
580        mTunePendding = false;
581    }
582
583    private void displayChannelBanner() {
584        runOnUiThread(new Runnable() {
585            @Override
586            public void run() {
587                if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
588                    return;
589                }
590
591                // TODO: Show a beautiful channel banner instead.
592                String channelBannerString = "";
593                String displayNumber = mChannelMap.getCurrentDisplayNumber();
594                if (displayNumber != null) {
595                    channelBannerString += displayNumber;
596                }
597                String displayName = mChannelMap.getCurrentDisplayName();
598                if (displayName != null) {
599                    channelBannerString += " " + displayName;
600                }
601                mChannelTextView.setText(channelBannerString);
602
603                Program program = TvInputUtils.getCurrentProgram(TvActivity.this,
604                        mChannelMap.getCurrentChannelUri());
605                String programTitle = program != null ? program.getTitle() : null;
606                // Program title might not be available at this point. Setting the text to null to
607                // clear the previous program title for now. It will be filled as soon as we get the
608                // updated program information.
609                mProgramTextView.setText(programTitle);
610
611                showAndHide(mChannelBanner, mHideChannelBanner, DURATION_SHOW_CHANNEL_BANNER);
612            }
613        });
614    }
615
616    public void showRecentlyWatchedDialog() {
617        showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
618                new RecentlyWatchedDialogFragment());
619    }
620
621    private void stopSession() {
622        if (mTvSession != null) {
623            mTvSession.setVolume(AUDIO_MIN_VOLUME);
624            mAudioManager.abandonAudioFocus(this);
625            mTvView.unbindTvInput();
626            mTvSession = null;
627            mTvInputInfo = null;
628        }
629        if (mChannelMap != null) {
630            mChannelMap.close();
631            mChannelMap = null;
632        }
633    }
634
635    private void stopPipSession() {
636        Log.d(TAG, "stopPipSession");
637        if (mPipSession != null) {
638            mPipView.setVisibility(View.INVISIBLE);
639            mPipView.unbindTvInput();
640            mPipSession = null;
641            mPipInputInfo = null;
642        }
643        mPipShowing = false;
644    }
645
646    @Override
647    protected void onSaveInstanceState(Bundle outState) {
648        // Do not save instance state because restoring instance state when TV app died
649        // unexpectedly can cause some problems like initializing fragments duplicately and
650        // accessing resource before it is initialzed.
651    }
652
653    @Override
654    protected void onDestroy() {
655        getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
656        mTvView.getHolder().removeCallback(mSurfaceHolderCallback);
657        mPipView.getHolder().removeCallback(mSurfaceHolderCallback);
658        Log.d(TAG, "onDestroy()");
659        super.onDestroy();
660    }
661
662    @Override
663    public boolean onKeyUp(int keyCode, KeyEvent event) {
664        if (mChannelMap == null) {
665            switch (keyCode) {
666                case KeyEvent.KEYCODE_H:
667                    showRecentlyWatchedDialog();
668                    return true;
669                case KeyEvent.KEYCODE_TV_INPUT:
670                case KeyEvent.KEYCODE_I:
671                case KeyEvent.KEYCODE_CHANNEL_UP:
672                case KeyEvent.KEYCODE_DPAD_UP:
673                case KeyEvent.KEYCODE_CHANNEL_DOWN:
674                case KeyEvent.KEYCODE_DPAD_DOWN:
675                case KeyEvent.KEYCODE_NUMPAD_ENTER:
676                case KeyEvent.KEYCODE_E:
677                case KeyEvent.KEYCODE_MENU:
678                    showInputPickerDialog();
679                    return true;
680            }
681        } else {
682            switch (keyCode) {
683                case KeyEvent.KEYCODE_H:
684                    showRecentlyWatchedDialog();
685                    return true;
686
687                case KeyEvent.KEYCODE_TV_INPUT:
688                case KeyEvent.KEYCODE_I:
689                    showInputPickerDialog();
690                    return true;
691
692                case KeyEvent.KEYCODE_CHANNEL_UP:
693                case KeyEvent.KEYCODE_DPAD_UP:
694                    channelUp();
695                    return true;
696
697                case KeyEvent.KEYCODE_CHANNEL_DOWN:
698                case KeyEvent.KEYCODE_DPAD_DOWN:
699                    channelDown();
700                    return true;
701
702                case KeyEvent.KEYCODE_NUMPAD_ENTER:
703                case KeyEvent.KEYCODE_E:
704                    displayChannelBanner();
705                    return true;
706
707                case KeyEvent.KEYCODE_DPAD_CENTER:
708                case KeyEvent.KEYCODE_MENU:
709                    if (event.isCanceled()) {
710                        return true;
711                    }
712                    showMenu();
713                    return true;
714            }
715        }
716        if (USE_DEBUG_KEYS) {
717            switch (keyCode) {
718                case KeyEvent.KEYCODE_W: {
719                    mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
720                    if (mDebugNonFullSizeScreen) {
721                        mTvView.layout(100, 100, 400, 300);
722                    } else {
723                        ViewGroup.LayoutParams params = mTvView.getLayoutParams();
724                        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
725                        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
726                        mTvView.setLayoutParams(params);
727                    }
728                    return true;
729                }
730                case KeyEvent.KEYCODE_P: {
731                    togglePipView();
732                    return true;
733                }
734                case KeyEvent.KEYCODE_CTRL_LEFT:
735                case KeyEvent.KEYCODE_CTRL_RIGHT: {
736                    mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
737                    return true;
738                }
739            }
740        }
741        return super.onKeyUp(keyCode, event);
742    }
743
744    @Override
745    public boolean onTouchEvent(MotionEvent event) {
746        mGestureDetector.onTouchEvent(event);
747        return super.onTouchEvent(event);
748    }
749
750    public void togglePipView() {
751        if (mPipShowing) {
752            stopPipSession();
753        } else {
754            startPipSession();
755        }
756    }
757
758    private boolean dispatchKeyEventToSession(final KeyEvent event) {
759        if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
760        if (mTvView != null) {
761            return mTvView.dispatchKeyEvent(event);
762        }
763        return false;
764    }
765
766    private void channelUp() {
767        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
768            mChannelMap.moveToNextChannel();
769            tune();
770        }
771    }
772
773    private void channelDown() {
774        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
775            mChannelMap.moveToPreviousChannel();
776            tune();
777        }
778    }
779
780    private void showMenu() {
781        MenuDialogFragment f = new MenuDialogFragment();
782        if (mTvSession != null) {
783            Bundle arg = new Bundle();
784            arg.putString(MenuDialogFragment.ARG_CURRENT_PACKAGE_NAME,
785                    mTvInputInfo.getPackageName());
786            arg.putString(MenuDialogFragment.ARG_CURRENT_SERVICE_NAME,
787                    mTvInputInfo.getServiceName());
788            f.setArguments(arg);
789        }
790
791        showDialogFragment(MenuDialogFragment.DIALOG_TAG, f);
792    }
793
794    private void showDialogFragment(String tag, DialogFragment dialog) {
795        FragmentTransaction ft = getFragmentManager().beginTransaction();
796        Fragment prev = getFragmentManager().findFragmentByTag(tag);
797        if (prev != null) {
798            ft.remove(prev);
799        }
800        ft.addToBackStack(null);
801        dialog.show(ft, tag);
802    }
803
804    private final Handler mHideHandler = new Handler();
805
806    private class HideRunnable implements Runnable {
807        private final View mView;
808
809        public HideRunnable(View view) {
810            mView = view;
811        }
812
813        @Override
814        public void run() {
815            mView.animate()
816                    .alpha(0f)
817                    .setDuration(mShortAnimationDuration)
818                    .setListener(new AnimatorListenerAdapter() {
819                        @Override
820                        public void onAnimationEnd(Animator animation) {
821                            mView.setVisibility(View.GONE);
822                        }
823                    });
824        }
825    }
826
827    private void showAndHide(View view, Runnable hide, long duration) {
828        if (view.getVisibility() == View.VISIBLE) {
829            // Skip the show animation if the view is already visible and cancel the scheduled hide
830            // animation.
831            mHideHandler.removeCallbacks(hide);
832        } else {
833            view.setAlpha(0f);
834            view.setVisibility(View.VISIBLE);
835            view.animate()
836                    .alpha(1f)
837                    .setDuration(mShortAnimationDuration)
838                    .setListener(null);
839        }
840        // Schedule the hide animation after a few seconds.
841        mHideHandler.postDelayed(hide, duration);
842    }
843
844    private void setShynessMode(boolean shyMode) {
845        mIsShy = shyMode;
846        Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST);
847        intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode);
848        sendBroadcast(intent);
849    }
850
851    private boolean isShyModeSet() {
852        return mIsShy;
853    }
854}
855