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 */ 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