Menu.java revision 48dadb49248271b01997862e1335912a4f2e189f
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.util.Log;
30
31import com.android.tv.ChannelTuner;
32import com.android.tv.R;
33import com.android.tv.TvApplication;
34import com.android.tv.analytics.DurationTimer;
35import com.android.tv.analytics.Tracker;
36import com.android.tv.common.TvCommonUtils;
37import com.android.tv.common.WeakHandler;
38import com.android.tv.data.Channel;
39import com.android.tv.menu.MenuRowFactory.PartnerRow;
40import com.android.tv.menu.MenuRowFactory.PipOptionsRow;
41import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
42
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45import java.util.ArrayList;
46import java.util.List;
47
48/**
49 * A class which controls the menu.
50 */
51public 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     */
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     */
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
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     */
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     */
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     */
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
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     */
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     */
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
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     */
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     */
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     */
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     */
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
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         */
321        public abstract void onMenuVisibilityChange(boolean visible);
322    }
323
324    private static class MenuWeakHandler extends WeakHandler<Menu> {
325        public MenuWeakHandler(Menu menu, Looper mainLooper) {
326            super(mainLooper, menu);
327        }
328
329        @Override
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