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