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