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