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