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 androidx.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                         : () -> {
217                             if (isActive()) {
218                                 mShowAnimator.start();
219                             }
220                         });
221         scheduleHide();
222     }
223 
224     /** Closes the menu. */
hide(boolean withAnimation)225     public void hide(boolean withAnimation) {
226         if (mShowAnimator.isStarted()) {
227             mShowAnimator.cancel();
228         }
229         if (!isActive()) {
230             return;
231         }
232         if (mAnimationDisabledForTest) {
233             withAnimation = false;
234         }
235         mAutoHideScheduler.cancel();
236         if (withAnimation) {
237             if (!mHideAnimator.isStarted()) {
238                 mHideAnimator.start();
239             }
240         } else if (mHideAnimator.isStarted()) {
241             // mMenuView.onHide() is called in AnimatorListener.
242             mHideAnimator.end();
243         } else {
244             hideInternal();
245         }
246     }
247 
hideInternal()248     private void hideInternal() {
249         mMenuView.onHide();
250         mTracker.sendHideMenu(mVisibleTimer.reset());
251         if (mOnMenuVisibilityChangeListener != null) {
252             mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false);
253         }
254     }
255 
256     /** Schedules to hide the menu in some seconds. */
scheduleHide()257     public void scheduleHide() {
258         mAutoHideScheduler.schedule(mShowDurationMillis);
259     }
260 
261     /**
262      * Called when the caller wants the main menu to be kept visible or not. If {@code keepVisible}
263      * is set to {@code true}, the hide schedule doesn't close the main menu, but calling {@link
264      * #hide} still hides it. If {@code keepVisible} is set to {@code false}, the hide schedule
265      * works as usual.
266      */
setKeepVisible(boolean keepVisible)267     public void setKeepVisible(boolean keepVisible) {
268         mKeepVisible = keepVisible;
269         if (mKeepVisible) {
270             mAutoHideScheduler.cancel();
271         } else if (isActive()) {
272             scheduleHide();
273         }
274     }
275 
276     @VisibleForTesting
isHideScheduled()277     boolean isHideScheduled() {
278         return mAutoHideScheduler.isScheduled();
279     }
280 
281     /** Returns {@code true} if the menu is open and not hiding. */
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     /** This method is called when channels are changed. */
onRecentChannelsChanged()307     public void onRecentChannelsChanged() {
308         if (DEBUG) Log.d(TAG, "onRecentChannelsChanged");
309         for (MenuRow row : mMenuRows) {
310             row.onRecentChannelsChanged();
311         }
312     }
313 
314     /** This method is called when the stream information is changed. */
onStreamInfoChanged()315     public void onStreamInfoChanged() {
316         if (DEBUG) Log.d(TAG, "update options row in main menu");
317         mMenuUpdater.onStreamInfoChanged();
318     }
319 
320     @Override
onAccessibilityStateChanged(boolean enabled)321     public void onAccessibilityStateChanged(boolean enabled) {
322         mAutoHideScheduler.onAccessibilityStateChanged(enabled);
323     }
324 
325     @VisibleForTesting
disableAnimationForTest()326     void disableAnimationForTest() {
327         if (!CommonUtils.isRunningInTest()) {
328             throw new RuntimeException("Animation may only be enabled/disabled during tests.");
329         }
330         mAnimationDisabledForTest = true;
331     }
332 
333     /** A listener which receives the notification when the menu is visible/invisible. */
334     public abstract static class OnMenuVisibilityChangeListener {
335         /** Called when the menu becomes visible/invisible. */
onMenuVisibilityChange(boolean visible)336         public abstract void onMenuVisibilityChange(boolean visible);
337     }
338 }
339