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