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
17package com.android.tv.menu;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.content.Context;
23import android.content.res.Resources;
24import android.os.Looper;
25import android.os.Message;
26import android.support.annotation.IntDef;
27import android.support.annotation.NonNull;
28import android.support.annotation.VisibleForTesting;
29import android.support.v17.leanback.widget.HorizontalGridView;
30import android.util.Log;
31
32import com.android.tv.ChannelTuner;
33import com.android.tv.R;
34import com.android.tv.TvApplication;
35import com.android.tv.TvOptionsManager;
36import com.android.tv.analytics.Tracker;
37import com.android.tv.common.TvCommonUtils;
38import com.android.tv.common.WeakHandler;
39import com.android.tv.menu.MenuRowFactory.PartnerRow;
40import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
41import com.android.tv.ui.TunableTvView;
42import com.android.tv.util.DurationTimer;
43import com.android.tv.util.ViewCache;
44
45import java.lang.annotation.Retention;
46import java.lang.annotation.RetentionPolicy;
47import java.util.ArrayList;
48import java.util.HashMap;
49import java.util.List;
50import java.util.Map;
51
52/**
53 * A class which controls the menu.
54 */
55public 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 {
90        PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1);
91        PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1);
92        PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1);
93        PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1);
94        PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS);
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
119    Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory,
120            OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
121        this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener);
122    }
123
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     */
156    public void setChannelTuner(ChannelTuner channelTuner) {
157        mMenuUpdater.setChannelTuner(channelTuner);
158    }
159
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     */
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     */
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     */
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     */
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
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
319    public void onStreamInfoChanged() {
320        if (DEBUG) Log.d(TAG, "update options row in main menu");
321        mMenuUpdater.onStreamInfoChanged();
322    }
323
324    @VisibleForTesting
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         */
339        public abstract void onMenuVisibilityChange(boolean visible);
340    }
341
342    private static class MenuWeakHandler extends WeakHandler<Menu> {
343        public MenuWeakHandler(Menu menu, Looper mainLooper) {
344            super(mainLooper, menu);
345        }
346
347        @Override
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