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
Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)104     Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory,
105             OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
106         this(context, null, menuView, menuRowFactory, onMenuVisibilityChangeListener);
107     }
108 
Menu(Context context, TunableTvView tvView, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)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      */
setChannelTuner(ChannelTuner channelTuner)141     public void setChannelTuner(ChannelTuner channelTuner) {
142         mMenuUpdater.setChannelTuner(channelTuner);
143     }
144 
addMenuRow(MenuRow row)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      */
release()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      */
show(@enuShowReason int reason)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      */
hide(boolean withAnimation)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 
hideInternal()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      */
scheduleHide()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      */
setKeepVisible(boolean keepVisible)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
isHideScheduled()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      */
isActive()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      */
update()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      */
update(String rowId)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      */
onRecentChannelsChanged()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      */
onStreamInfoChanged()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
disableAnimationForTest()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          */
onMenuVisibilityChange(boolean visible)316         public abstract void onMenuVisibilityChange(boolean visible);
317     }
318 
319     private static class MenuWeakHandler extends WeakHandler<Menu> {
MenuWeakHandler(Menu menu, Looper mainLooper)320         public MenuWeakHandler(Menu menu, Looper mainLooper) {
321             super(mainLooper, menu);
322         }
323 
324         @Override
handleMessage(Message msg, @NonNull Menu menu)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