• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1)90         PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1);
PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1)91         PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1);
PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1)92         PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1);
PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1)93         PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1);
PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS)94         PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS);
PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7)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
Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)119     Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory,
120             OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
121         this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener);
122     }
123 
Menu(Context context, TunableTvView tvView, TvOptionsManager optionsManager, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)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      */
setChannelTuner(ChannelTuner channelTuner)156     public void setChannelTuner(ChannelTuner channelTuner) {
157         mMenuUpdater.setChannelTuner(channelTuner);
158     }
159 
addMenuRow(MenuRow row)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      */
release()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      */
preloadItemViews()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      */
show(@enuShowReason int reason)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      */
hide(boolean withAnimation)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 
hideInternal()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      */
scheduleHide()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      */
setKeepVisible(boolean keepVisible)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
isHideScheduled()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      */
isActive()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      */
update()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      */
update(String rowId)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      */
onRecentChannelsChanged()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      */
onStreamInfoChanged()319     public void onStreamInfoChanged() {
320         if (DEBUG) Log.d(TAG, "update options row in main menu");
321         mMenuUpdater.onStreamInfoChanged();
322     }
323 
324     @VisibleForTesting
disableAnimationForTest()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          */
onMenuVisibilityChange(boolean visible)339         public abstract void onMenuVisibilityChange(boolean visible);
340     }
341 
342     private static class MenuWeakHandler extends WeakHandler<Menu> {
MenuWeakHandler(Menu menu, Looper mainLooper)343         public MenuWeakHandler(Menu menu, Looper mainLooper) {
344             super(mainLooper, menu);
345         }
346 
347         @Override
handleMessage(Message msg, @NonNull Menu menu)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