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