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