Home | History | Annotate | Download | only in menu
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.tv.menu;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorInflater;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.os.Looper;
     25 import android.os.Message;
     26 import android.support.annotation.IntDef;
     27 import android.support.annotation.NonNull;
     28 import android.support.annotation.VisibleForTesting;
     29 import android.util.Log;
     30 
     31 import com.android.tv.ChannelTuner;
     32 import com.android.tv.R;
     33 import com.android.tv.TvApplication;
     34 import com.android.tv.analytics.DurationTimer;
     35 import com.android.tv.analytics.Tracker;
     36 import com.android.tv.common.TvCommonUtils;
     37 import com.android.tv.common.WeakHandler;
     38 import com.android.tv.data.Channel;
     39 import com.android.tv.menu.MenuRowFactory.PartnerRow;
     40 import com.android.tv.menu.MenuRowFactory.PipOptionsRow;
     41 import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
     42 
     43 import java.lang.annotation.Retention;
     44 import java.lang.annotation.RetentionPolicy;
     45 import java.util.ArrayList;
     46 import java.util.List;
     47 
     48 /**
     49  * A class which controls the menu.
     50  */
     51 public class Menu {
     52     private static final String TAG = "Menu";
     53     private static final boolean DEBUG = false;
     54 
     55     @Retention(RetentionPolicy.SOURCE)
     56     @IntDef({REASON_NONE, REASON_GUIDE, REASON_PLAY_CONTROLS_PLAY, REASON_PLAY_CONTROLS_PAUSE,
     57         REASON_PLAY_CONTROLS_PLAY_PAUSE, REASON_PLAY_CONTROLS_REWIND,
     58         REASON_PLAY_CONTROLS_FAST_FORWARD, REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS,
     59         REASON_PLAY_CONTROLS_JUMP_TO_NEXT, REASON_RECORDING_PLAYBACK})
     60     public @interface MenuShowReason {}
     61     public static final int REASON_NONE = 0;
     62     public static final int REASON_GUIDE = 1;
     63     public static final int REASON_PLAY_CONTROLS_PLAY = 2;
     64     public static final int REASON_PLAY_CONTROLS_PAUSE = 3;
     65     public static final int REASON_PLAY_CONTROLS_PLAY_PAUSE = 4;
     66     public static final int REASON_PLAY_CONTROLS_REWIND = 5;
     67     public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6;
     68     public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7;
     69     public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8;
     70     public static final int REASON_RECORDING_PLAYBACK = 9;
     71 
     72     private static final List<String> sRowIdListForReason = new ArrayList<>();
     73     static {
     74         sRowIdListForReason.add(null);  // REASON_NONE
     75         sRowIdListForReason.add(ChannelsRow.ID);  // REASON_GUIDE
     76         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_PLAY
     77         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_PAUSE
     78         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_PLAY_PAUSE
     79         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_REWIND
     80         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_FAST_FORWARD
     81         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS
     82         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_PLAY_CONTROLS_JUMP_TO_NEXT
     83         sRowIdListForReason.add(PlayControlsRow.ID);  // REASON_RECORDING_PLAYBACK
     84     }
     85 
     86     private static final String SCREEN_NAME = "Menu";
     87 
     88     private static final int MSG_HIDE_MENU = 1000;
     89 
     90     private final IMenuView mMenuView;
     91     private final Tracker mTracker;
     92     private final DurationTimer mVisibleTimer = new DurationTimer();
     93     private final long mShowDurationMillis;
     94     private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener;
     95     private final WeakHandler<Menu> mHandler = new MenuWeakHandler(this, Looper.getMainLooper());
     96 
     97     private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
     98         @Override
     99         public void onLoadFinished() {}
    100 
    101         @Override
    102         public void onBrowsableChannelListChanged() {
    103             mMenuView.update(isActive());
    104         }
    105 
    106         @Override
    107         public void onCurrentChannelUnavailable(Channel channel) {}
    108 
    109         @Override
    110         public void onChannelChanged(Channel previousChannel, Channel currentChannel) {}
    111     };
    112 
    113     private final List<MenuRow> mMenuRows = new ArrayList<>();
    114     private final Animator mShowAnimator;
    115     private final Animator mHideAnimator;
    116 
    117     private ChannelTuner mChannelTuner;
    118     private boolean mKeepVisible;
    119     private boolean mAnimationDisabledForTest;
    120 
    121     /**
    122      * A constructor.
    123      */
    124     public Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory,
    125                 OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
    126         mMenuView = menuView;
    127         mTracker = TvApplication.getSingletons(context).getTracker();
    128         Resources res = context.getResources();
    129         mShowDurationMillis = res.getInteger(R.integer.menu_show_duration);
    130         mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener;
    131         mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter);
    132         mShowAnimator.setTarget(mMenuView);
    133         mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit);
    134         mHideAnimator.addListener(new AnimatorListenerAdapter() {
    135             @Override
    136             public void onAnimationEnd(Animator animation) {
    137                 hideInternal();
    138             }
    139         });
    140         mHideAnimator.setTarget(mMenuView);
    141         // Build menu rows
    142         addMenuRow(menuRowFactory.createMenuRow(this, PlayControlsRow.class));
    143         addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class));
    144         addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class));
    145         addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class));
    146         addMenuRow(menuRowFactory.createMenuRow(this, PipOptionsRow.class));
    147         mMenuView.setMenuRows(mMenuRows);
    148     }
    149 
    150     /**
    151      * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready
    152      * or not available any more.
    153      */
    154     public void setChannelTuner(ChannelTuner channelTuner) {
    155         if (mChannelTuner != null) {
    156             mChannelTuner.removeListener(mChannelTunerListener);
    157         }
    158         mChannelTuner = channelTuner;
    159         if (mChannelTuner != null) {
    160             mChannelTuner.addListener(mChannelTunerListener);
    161         }
    162         mMenuView.update(isActive());
    163     }
    164 
    165     private void addMenuRow(MenuRow row) {
    166         if (row != null) {
    167             mMenuRows.add(row);
    168         }
    169     }
    170 
    171     /**
    172      * Call this method to end the lifetime of the menu.
    173      */
    174     public void release() {
    175         setChannelTuner(null);
    176         for (MenuRow row : mMenuRows) {
    177             row.release();
    178         }
    179         mHandler.removeCallbacksAndMessages(null);
    180     }
    181 
    182     /**
    183      * Shows the main menu.
    184      *
    185      * @param reason A reason why this is called. See {@link MenuShowReason}
    186      */
    187     public void show(@MenuShowReason int reason) {
    188         if (DEBUG) Log.d(TAG, "show reason:" + reason);
    189         mTracker.sendShowMenu();
    190         mVisibleTimer.start();
    191         mTracker.sendScreenView(SCREEN_NAME);
    192         if (mHideAnimator.isStarted()) {
    193             mHideAnimator.end();
    194         }
    195         if (mOnMenuVisibilityChangeListener != null) {
    196             mOnMenuVisibilityChangeListener.onMenuVisibilityChange(true);
    197         }
    198         String rowIdToSelect = sRowIdListForReason.get(reason);
    199         mMenuView.onShow(reason, rowIdToSelect, mAnimationDisabledForTest ? null : new Runnable() {
    200             @Override
    201             public void run() {
    202                 mShowAnimator.start();
    203             }
    204         });
    205         scheduleHide();
    206     }
    207 
    208     /**
    209      * Closes the menu.
    210      */
    211     public void hide(boolean withAnimation) {
    212         if (!isActive()) {
    213             return;
    214         }
    215         if (mAnimationDisabledForTest) {
    216             withAnimation = false;
    217         }
    218         mHandler.removeMessages(MSG_HIDE_MENU);
    219         if (withAnimation) {
    220             if (!mHideAnimator.isStarted()) {
    221                 mHideAnimator.start();
    222             }
    223         } else if (mHideAnimator.isStarted()) {
    224             // mMenuView.onHide() is called in AnimatorListener.
    225             mHideAnimator.end();
    226         } else {
    227             hideInternal();
    228         }
    229     }
    230 
    231     private void hideInternal() {
    232         mMenuView.onHide();
    233         mTracker.sendHideMenu(mVisibleTimer.reset());
    234         if (mOnMenuVisibilityChangeListener != null) {
    235             mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false);
    236         }
    237     }
    238 
    239     /**
    240      * Schedules to hide the menu in some seconds.
    241      */
    242     public void scheduleHide() {
    243         mHandler.removeMessages(MSG_HIDE_MENU);
    244         if (!mKeepVisible) {
    245             mHandler.sendEmptyMessageDelayed(MSG_HIDE_MENU, mShowDurationMillis);
    246         }
    247     }
    248 
    249     /**
    250      * Called when the caller wants the main menu to be kept visible or not.
    251      * If {@code keepVisible} is set to {@code true}, the hide schedule doesn't close the main menu,
    252      * but calling {@link #hide} still hides it.
    253      * If {@code keepVisible} is set to {@code false}, the hide schedule works as usual.
    254      */
    255     public void setKeepVisible(boolean keepVisible) {
    256         mKeepVisible = keepVisible;
    257         if (mKeepVisible) {
    258             mHandler.removeMessages(MSG_HIDE_MENU);
    259         } else if (isActive()) {
    260             scheduleHide();
    261         }
    262     }
    263 
    264     @VisibleForTesting
    265     boolean isHideScheduled() {
    266         return mHandler.hasMessages(MSG_HIDE_MENU);
    267     }
    268 
    269     /**
    270      * Returns {@code true} if the menu is open and not hiding.
    271      */
    272     public boolean isActive() {
    273         return mMenuView.isVisible() && !mHideAnimator.isStarted();
    274     }
    275 
    276     /**
    277      * Updates menu contents.
    278      *
    279      * <p>Returns <@code true> if the contents have been changed, otherwise {@code false}.
    280      */
    281     public boolean update() {
    282         if (DEBUG) Log.d(TAG, "update main menu");
    283         return mMenuView.update(isActive());
    284     }
    285 
    286     /**
    287      * This method is called when channels are changed.
    288      */
    289     public void onRecentChannelsChanged() {
    290         if (DEBUG) Log.d(TAG, "onRecentChannelsChanged");
    291         for (MenuRow row : mMenuRows) {
    292             row.onRecentChannelsChanged();
    293         }
    294     }
    295 
    296     /**
    297      * This method is called when the stream information is changed.
    298      */
    299     public void onStreamInfoChanged() {
    300         if (DEBUG) Log.d(TAG, "update options row in main menu");
    301         for (MenuRow row : mMenuRows) {
    302             row.onStreamInfoChanged();
    303         }
    304     }
    305 
    306     @VisibleForTesting
    307     void disableAnimationForTest() {
    308         if (!TvCommonUtils.isRunningInTest()) {
    309             throw new RuntimeException("Animation may only be enabled/disabled during tests.");
    310         }
    311         mAnimationDisabledForTest = true;
    312     }
    313 
    314     /**
    315      * A listener which receives the notification when the menu is visible/invisible.
    316      */
    317     public static abstract class OnMenuVisibilityChangeListener {
    318         /**
    319          * Called when the menu becomes visible/invisible.
    320          */
    321         public abstract void onMenuVisibilityChange(boolean visible);
    322     }
    323 
    324     private static class MenuWeakHandler extends WeakHandler<Menu> {
    325         public MenuWeakHandler(Menu menu, Looper mainLooper) {
    326             super(mainLooper, menu);
    327         }
    328 
    329         @Override
    330         public void handleMessage(Message msg, @NonNull Menu menu) {
    331             if (msg.what == MSG_HIDE_MENU) {
    332                 menu.hide(true);
    333             }
    334         }
    335     }
    336 }
    337