TvActivity.java revision 5980d6736ab06e8c15eebb151a3b0c8d943b37b8
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        if (!isShyModeSet()) {
321            setShynessMode(true);
322        }
323        super.onStop();
324    }
325
326    public void showInputPickerDialog() {
327        InputPickerDialogFragment f = new InputPickerDialogFragment();
328        Bundle arg = new Bundle();
329        if (mTvInputInfo != null) {
330            arg.putString(InputPickerDialogFragment.ARG_MAIN_INPUT_ID, mTvInputInfo.getId());
331        }
332        if (mPipInputInfo != null) {
333            arg.putString(InputPickerDialogFragment.ARG_SUB_INPUT_ID, mPipInputInfo.getId());
334        }
335        f.setArguments(arg);
336        showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, f);
337    }
338
339    @Override
340    public void onInputPicked(final TvInputInfo selectedTvInput, final String displayName) {
341        if (mTvSession != null && selectedTvInput.equals(mTvInputInfo)) {
342            // Nothing has changed thus nothing to do.
343            return;
344        }
345
346        if (!TvInputUtils.hasChannel(this, selectedTvInput)) {
347            mTvInputInfoForSetup = null;
348            if (showSetupActivity(selectedTvInput, displayName)) {
349                stopSession();
350            }
351            return;
352        }
353
354        // Start a new session with the new input.
355        stopSession();
356
357        // TODO: It is a hack to wait to release a surface at TIS. If there is a way to
358        // know when the surface is released at TIS, we don't need this hack.
359        mHandler.postDelayed(new Runnable() {
360            @Override
361            public void run() {
362                startSession(selectedTvInput);
363            }
364        }, DELAY_FOR_SURFACE_RELEASE);
365    }
366
367    private boolean showSetupActivity(TvInputInfo inputInfo, String displayName) {
368        PackageManager pm = getPackageManager();
369        List<ResolveInfo> activityInfos = pm.queryIntentActivities(
370                new Intent(TvInputUtils.ACTION_SETUP), PackageManager.GET_ACTIVITIES);
371        ResolveInfo setupActivity = null;
372        if (activityInfos != null) {
373            for (ResolveInfo info : activityInfos) {
374                if (info.activityInfo.packageName.equals(inputInfo.getPackageName())) {
375                    setupActivity = info;
376                }
377            }
378        }
379
380        if (setupActivity == null) {
381            String message = String.format(getString(R.string.input_setup_activity_not_found),
382                    displayName);
383            new AlertDialog.Builder(this)
384                    .setMessage(message)
385                    .setPositiveButton(R.string.OK, null)
386                    .show();
387            return false;
388        }
389
390        mTvInputInfoForSetup = inputInfo;
391        Intent intent = new Intent(TvInputUtils.ACTION_SETUP);
392        intent.setClassName(setupActivity.activityInfo.packageName,
393                setupActivity.activityInfo.name);
394        startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY);
395
396        return true;
397    }
398
399    @Override
400    public void onActivityResult(int requestCode, int resultCode, Intent data) {
401        switch (requestCode) {
402            case REQUEST_START_SETUP_ACTIIVTY:
403                if (resultCode == Activity.RESULT_OK && mTvInputInfoForSetup != null) {
404                    startSession(mTvInputInfoForSetup);
405                }
406                break;
407
408            default:
409                //TODO: Handle failure of setup.
410        }
411        mTvInputInfoForSetup = null;
412    }
413
414    @Override
415    public boolean dispatchKeyEvent(KeyEvent event) {
416        if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
417        int eventKeyCode = event.getKeyCode();
418        if (mUseKeycodeBlacklist) {
419            for (int keycode : KEYCODE_BLACKLIST) {
420                if (keycode == eventKeyCode) {
421                    return super.dispatchKeyEvent(event);
422                }
423            }
424            return dispatchKeyEventToSession(event);
425        } else {
426            for (int keycode : KEYCODE_WHITELIST) {
427                if (keycode == eventKeyCode) {
428                    return dispatchKeyEventToSession(event);
429                }
430            }
431            return super.dispatchKeyEvent(event);
432        }
433    }
434
435    @Override
436    public void onAudioFocusChange(int focusChange) {
437        mAudioFocusStatus = focusChange;
438        setVolumeByAudioFocusStatus();
439    }
440
441    private void setVolumeByAudioFocusStatus() {
442        if (mTvSession != null) {
443            switch (mAudioFocusStatus) {
444                case AudioManager.AUDIOFOCUS_GAIN:
445                    mTvSession.setVolume(AUDIO_MAX_VOLUME);
446                    break;
447                case AudioManager.AUDIOFOCUS_LOSS:
448                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
449                    mTvSession.setVolume(AUDIO_MIN_VOLUME);
450                    break;
451                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
452                    mTvSession.setVolume(AUDIO_DUCKING_VOLUME);
453                    break;
454            }
455        }
456    }
457
458    private void startSession(TvInputInfo selectedTvInput) {
459        long channelId = TvInputUtils.getLastWatchedChannelId(TvActivity.this,
460                selectedTvInput.getId());
461        startSession(selectedTvInput, channelId);
462    }
463
464    private void startSession(TvInputInfo inputInfo, long channelId) {
465        // TODO: recreate SurfaceView to prevent abusing from the previous session.
466        mTvInputInfo = inputInfo;
467        // Prepare a new channel map for the current input.
468        mChannelMap = new ChannelMap(this, inputInfo.getComponent(), channelId,
469                mOnChannelsLoadFinished);
470        // Create a new session and start.
471        mTvView.bindTvInput(inputInfo.getComponent(), mSessionCreated);
472        tune();
473    }
474
475    private final TvInputManager.SessionCreateCallback mSessionCreated =
476            new TvInputManager.SessionCreateCallback() {
477                @Override
478                public void onSessionCreated(TvInputManager.Session session) {
479                    if (session != null) {
480                        mTvSession = session;
481                        int result = mAudioManager.requestAudioFocus(TvActivity.this,
482                                AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
483                        mAudioFocusStatus =
484                                (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
485                                        AudioManager.AUDIOFOCUS_GAIN
486                                        : AudioManager.AUDIOFOCUS_LOSS;
487                        if (mTunePendding) {
488                            tune();
489                        }
490                    } else {
491                        Log.w(TAG, "Failed to create a session");
492                        // TODO: show something to user about this error.
493                    }
494                }
495            };
496
497    private void startPipSession() {
498        if (mTvSession == null) {
499            Log.w(TAG, "TV content should be playing.");
500            return;
501        }
502        Log.d(TAG, "startPipSession");
503        mPipInputInfo = mTvInputInfo;
504        mPipView.bindTvInput(mPipInputInfo.getComponent(), mPipSessionCreated);
505        mPipShowing = true;
506    }
507
508    private final TvInputManager.SessionCreateCallback mPipSessionCreated =
509            new TvInputManager.SessionCreateCallback() {
510                @Override
511                public void onSessionCreated(final TvInputManager.Session session) {
512                    Log.d(TAG, "PIP session is created.");
513                    if (mTvSession == null) {
514                        Log.w(TAG, "TV content should be playing.");
515                        if (session != null) {
516                            mPipView.unbindTvInput();
517                        }
518                        mPipShowing = false;
519                        return;
520                    }
521                    if (session == null) {
522                        Log.w(TAG, "Fail to create another session.");
523                        mPipShowing = false;
524                        return;
525                    }
526                    runOnUiThread(new Runnable() {
527                        @Override
528                        public void run() {
529                            mPipSession = session;
530                            mPipSession.setVolume(0);
531                            mPipSession.tune(mChannelMap.getCurrentChannelUri());
532                            mPipView.setVisibility(View.VISIBLE);
533                        }
534                    });
535                }
536            };
537
538    private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) {
539        @Override
540        public void onChange(boolean selfChange, Uri uri) {
541            if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
542                return;
543            }
544            Uri channelUri = mChannelMap.getCurrentChannelUri();
545            if (channelUri == null) {
546                return;
547            }
548            Program program = TvInputUtils.getCurrentProgram(TvActivity.this, channelUri);
549            if (program == null) {
550                return;
551            }
552            mProgramTextView.setText(program.getTitle());
553        }
554    };
555
556    private final Runnable mOnChannelsLoadFinished = new Runnable() {
557        @Override
558        public void run() {
559            if (mTunePendding) {
560                tune();
561            }
562        }
563    };
564
565    private void tune() {
566        Log.d(TAG, "tune()");
567        // Prerequisites to be able to tune.
568        if (mChannelMap == null || !mChannelMap.isLoadFinished()) {
569            Log.d(TAG, "Channel map not ready");
570            mTunePendding = true;
571            return;
572        }
573        if (mTvSession == null) {
574            Log.d(TAG, "Service not connected");
575            mTunePendding = true;
576            return;
577        }
578        setVolumeByAudioFocusStatus();
579
580        Uri currentChannelUri = mChannelMap.getCurrentChannelUri();
581        if (currentChannelUri != null) {
582            // TODO: implement 'no signal'
583            // TODO: add result callback and show a message on failure.
584            TvInputUtils.setLastWatchedChannel(this, mTvInputInfo.getId(), currentChannelUri);
585            mTvSession.tune(currentChannelUri);
586            if (isShyModeSet()) {
587                setShynessMode(false);
588                // TODO: Set the shy mode to true when tune() fails.
589            }
590            displayChannelBanner();
591        }
592        mTunePendding = false;
593    }
594
595    private void displayChannelBanner() {
596        runOnUiThread(new Runnable() {
597            @Override
598            public void run() {
599                // TODO: Show a beautiful channel banner instead.
600                String channelBannerString = "";
601                String displayNumber = mChannelMap.getCurrentDisplayNumber();
602                if (displayNumber != null) {
603                    channelBannerString += displayNumber;
604                }
605                String displayName = mChannelMap.getCurrentDisplayName();
606                if (displayName != null) {
607                    channelBannerString += " " + displayName;
608                }
609                mChannelTextView.setText(channelBannerString);
610
611                Program program = TvInputUtils.getCurrentProgram(TvActivity.this,
612                        mChannelMap.getCurrentChannelUri());
613                String programTitle = program != null ? program.getTitle() : null;
614                // Program title might not be available at this point. Setting the text to null to
615                // clear the previous program title for now. It will be filled as soon as we get the
616                // updated program information.
617                mProgramTextView.setText(programTitle);
618
619                showAndHide(mChannelBanner, mHideChannelBanner, DURATION_SHOW_CHANNEL_BANNER);
620            }
621        });
622    }
623
624    public void showRecentlyWatchedDialog() {
625        showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
626                new RecentlyWatchedDialogFragment());
627    }
628
629    private void stopSession() {
630        if (mTvSession != null) {
631            mTvSession.setVolume(AUDIO_MIN_VOLUME);
632            mAudioManager.abandonAudioFocus(this);
633            mTvView.unbindTvInput();
634            mTvSession = null;
635            mTvInputInfo = null;
636        }
637        if (mChannelMap != null) {
638            mChannelMap.close();
639            mChannelMap = null;
640        }
641    }
642
643    private void stopPipSession() {
644        Log.d(TAG, "stopPipSession");
645        if (mPipSession != null) {
646            mPipView.setVisibility(View.INVISIBLE);
647            mPipView.unbindTvInput();
648            mPipSession = null;
649            mPipInputInfo = null;
650        }
651        mPipShowing = false;
652    }
653
654    @Override
655    protected void onSaveInstanceState(Bundle outState) {
656        // Do not save instance state because restoring instance state when TV app died
657        // unexpectedly can cause some problems like initializing fragments duplicately and
658        // accessing resource before it is initialzed.
659    }
660
661    @Override
662    protected void onDestroy() {
663        getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
664        mTvView.getHolder().removeCallback(mSurfaceHolderCallback);
665        mPipView.getHolder().removeCallback(mSurfaceHolderCallback);
666        Log.d(TAG, "onDestroy()");
667        super.onDestroy();
668    }
669
670    @Override
671    public boolean onKeyUp(int keyCode, KeyEvent event) {
672        if (mChannelMap == null) {
673            switch (keyCode) {
674                case KeyEvent.KEYCODE_H:
675                    showRecentlyWatchedDialog();
676                    return true;
677                case KeyEvent.KEYCODE_TV_INPUT:
678                case KeyEvent.KEYCODE_I:
679                case KeyEvent.KEYCODE_CHANNEL_UP:
680                case KeyEvent.KEYCODE_DPAD_UP:
681                case KeyEvent.KEYCODE_CHANNEL_DOWN:
682                case KeyEvent.KEYCODE_DPAD_DOWN:
683                case KeyEvent.KEYCODE_NUMPAD_ENTER:
684                case KeyEvent.KEYCODE_E:
685                case KeyEvent.KEYCODE_MENU:
686                    showInputPickerDialog();
687                    return true;
688            }
689        } else {
690            switch (keyCode) {
691                case KeyEvent.KEYCODE_H:
692                    showRecentlyWatchedDialog();
693                    return true;
694
695                case KeyEvent.KEYCODE_TV_INPUT:
696                case KeyEvent.KEYCODE_I:
697                    showInputPickerDialog();
698                    return true;
699
700                case KeyEvent.KEYCODE_CHANNEL_UP:
701                case KeyEvent.KEYCODE_DPAD_UP:
702                    channelUp();
703                    return true;
704
705                case KeyEvent.KEYCODE_CHANNEL_DOWN:
706                case KeyEvent.KEYCODE_DPAD_DOWN:
707                    channelDown();
708                    return true;
709
710                case KeyEvent.KEYCODE_NUMPAD_ENTER:
711                case KeyEvent.KEYCODE_E:
712                    displayChannelBanner();
713                    return true;
714
715                case KeyEvent.KEYCODE_DPAD_CENTER:
716                case KeyEvent.KEYCODE_MENU:
717                    if (event.isCanceled()) {
718                        return true;
719                    }
720                    showMenu();
721                    return true;
722            }
723        }
724        if (USE_DEBUG_KEYS) {
725            switch (keyCode) {
726                case KeyEvent.KEYCODE_W: {
727                    mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
728                    if (mDebugNonFullSizeScreen) {
729                        mTvView.layout(100, 100, 400, 300);
730                    } else {
731                        ViewGroup.LayoutParams params = mTvView.getLayoutParams();
732                        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
733                        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
734                        mTvView.setLayoutParams(params);
735                    }
736                    return true;
737                }
738                case KeyEvent.KEYCODE_P: {
739                    togglePipView();
740                    return true;
741                }
742                case KeyEvent.KEYCODE_CTRL_LEFT:
743                case KeyEvent.KEYCODE_CTRL_RIGHT: {
744                    mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
745                    return true;
746                }
747            }
748        }
749        return super.onKeyUp(keyCode, event);
750    }
751
752    @Override
753    public boolean onTouchEvent(MotionEvent event) {
754        mGestureDetector.onTouchEvent(event);
755        return super.onTouchEvent(event);
756    }
757
758    public void togglePipView() {
759        if (mPipShowing) {
760            stopPipSession();
761        } else {
762            startPipSession();
763        }
764    }
765
766    private boolean dispatchKeyEventToSession(final KeyEvent event) {
767        if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
768        if (mTvView != null) {
769            return mTvView.dispatchKeyEvent(event);
770        }
771        return false;
772    }
773
774    private void channelUp() {
775        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
776            mChannelMap.moveToNextChannel();
777            tune();
778        }
779    }
780
781    private void channelDown() {
782        if (mChannelMap != null && mChannelMap.isLoadFinished()) {
783            mChannelMap.moveToPreviousChannel();
784            tune();
785        }
786    }
787
788    private void showMenu() {
789        MenuDialogFragment f = new MenuDialogFragment();
790        if (mTvSession != null) {
791            Bundle arg = new Bundle();
792            arg.putParcelable(MenuDialogFragment.ARG_CURRENT_INPUT, mTvInputInfo);
793            f.setArguments(arg);
794        }
795
796        showDialogFragment(MenuDialogFragment.DIALOG_TAG, f);
797    }
798
799    private void showDialogFragment(String tag, DialogFragment dialog) {
800        FragmentTransaction ft = getFragmentManager().beginTransaction();
801        Fragment prev = getFragmentManager().findFragmentByTag(tag);
802        if (prev != null) {
803            ft.remove(prev);
804        }
805        ft.addToBackStack(null);
806        dialog.show(ft, tag);
807    }
808
809    private final Handler mHideHandler = new Handler();
810
811    private class HideRunnable implements Runnable {
812        private final View mView;
813
814        public HideRunnable(View view) {
815            mView = view;
816        }
817
818        @Override
819        public void run() {
820            mView.animate()
821                    .alpha(0f)
822                    .setDuration(mShortAnimationDuration)
823                    .setListener(new AnimatorListenerAdapter() {
824                        @Override
825                        public void onAnimationEnd(Animator animation) {
826                            mView.setVisibility(View.GONE);
827                        }
828                    });
829        }
830    }
831
832    private void showAndHide(View view, Runnable hide, long duration) {
833        if (view.getVisibility() == View.VISIBLE) {
834            // Skip the show animation if the view is already visible and cancel the scheduled hide
835            // animation.
836            mHideHandler.removeCallbacks(hide);
837        } else {
838            view.setAlpha(0f);
839            view.setVisibility(View.VISIBLE);
840            view.animate()
841                    .alpha(1f)
842                    .setDuration(mShortAnimationDuration)
843                    .setListener(null);
844        }
845        // Schedule the hide animation after a few seconds.
846        mHideHandler.postDelayed(hide, duration);
847    }
848
849    private void setShynessMode(boolean shyMode) {
850        mIsShy = shyMode;
851        Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST);
852        intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode);
853        sendBroadcast(intent);
854    }
855
856    private boolean isShyModeSet() {
857        return mIsShy;
858    }
859}
860