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.support.annotation.IntDef; 25 import android.support.annotation.VisibleForTesting; 26 import androidx.leanback.widget.HorizontalGridView; 27 import android.util.Log; 28 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; 29 import com.android.tv.ChannelTuner; 30 import com.android.tv.R; 31 import com.android.tv.TvOptionsManager; 32 import com.android.tv.TvSingletons; 33 import com.android.tv.analytics.Tracker; 34 import com.android.tv.common.util.CommonUtils; 35 import com.android.tv.common.util.DurationTimer; 36 import com.android.tv.menu.MenuRowFactory.PartnerRow; 37 import com.android.tv.menu.MenuRowFactory.TvOptionsRow; 38 import com.android.tv.ui.TunableTvView; 39 import com.android.tv.ui.hideable.AutoHideScheduler; 40 import com.android.tv.util.ViewCache; 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 /** A class which controls the menu. */ 49 public class Menu implements AccessibilityStateChangeListener { 50 private static final String TAG = "Menu"; 51 private static final boolean DEBUG = false; 52 53 @Retention(RetentionPolicy.SOURCE) 54 @IntDef({ 55 REASON_NONE, 56 REASON_GUIDE, 57 REASON_PLAY_CONTROLS_PLAY, 58 REASON_PLAY_CONTROLS_PAUSE, 59 REASON_PLAY_CONTROLS_PLAY_PAUSE, 60 REASON_PLAY_CONTROLS_REWIND, 61 REASON_PLAY_CONTROLS_FAST_FORWARD, 62 REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, 63 REASON_PLAY_CONTROLS_JUMP_TO_NEXT 64 }) 65 public @interface MenuShowReason {} 66 67 public static final int REASON_NONE = 0; 68 public static final int REASON_GUIDE = 1; 69 public static final int REASON_PLAY_CONTROLS_PLAY = 2; 70 public static final int REASON_PLAY_CONTROLS_PAUSE = 3; 71 public static final int REASON_PLAY_CONTROLS_PLAY_PAUSE = 4; 72 public static final int REASON_PLAY_CONTROLS_REWIND = 5; 73 public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6; 74 public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7; 75 public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; 76 77 private static final List<String> sRowIdListForReason = new ArrayList<>(); 78 79 static { 80 sRowIdListForReason.add(null); // REASON_NONE 81 sRowIdListForReason.add(ChannelsRow.ID); // REASON_GUIDE 82 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY 83 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PAUSE 84 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY_PAUSE 85 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_REWIND 86 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD 87 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS 88 sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT 89 } 90 91 private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>(); 92 93 static { PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1)94 PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1); PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1)95 PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1); PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1)96 PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1); PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1)97 PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1); PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS)98 PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS); PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7)99 PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7); 100 } 101 102 private static final String SCREEN_NAME = "Menu"; 103 104 private final Context mContext; 105 private final IMenuView mMenuView; 106 private final Tracker mTracker; 107 private final DurationTimer mVisibleTimer = new DurationTimer(); 108 private final long mShowDurationMillis; 109 private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener; 110 private final AutoHideScheduler mAutoHideScheduler; 111 112 private final MenuUpdater mMenuUpdater; 113 private final List<MenuRow> mMenuRows = new ArrayList<>(); 114 private final Animator mShowAnimator; 115 private final Animator mHideAnimator; 116 117 private boolean mKeepVisible; 118 private boolean mAnimationDisabledForTest; 119 120 @VisibleForTesting Menu( Context context, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)121 Menu( 122 Context context, 123 IMenuView menuView, 124 MenuRowFactory menuRowFactory, 125 OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { 126 this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); 127 } 128 Menu( Context context, TunableTvView tvView, TvOptionsManager optionsManager, IMenuView menuView, MenuRowFactory menuRowFactory, OnMenuVisibilityChangeListener onMenuVisibilityChangeListener)129 public Menu( 130 Context context, 131 TunableTvView tvView, 132 TvOptionsManager optionsManager, 133 IMenuView menuView, 134 MenuRowFactory menuRowFactory, 135 OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { 136 mContext = context; 137 mMenuView = menuView; 138 mTracker = TvSingletons.getSingletons(context).getTracker(); 139 mMenuUpdater = new MenuUpdater(this, tvView, optionsManager); 140 Resources res = context.getResources(); 141 mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); 142 mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; 143 mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter); 144 mShowAnimator.setTarget(mMenuView); 145 mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit); 146 mHideAnimator.addListener( 147 new AnimatorListenerAdapter() { 148 @Override 149 public void onAnimationEnd(Animator animation) { 150 hideInternal(); 151 } 152 }); 153 mHideAnimator.setTarget(mMenuView); 154 // Build menu rows 155 addMenuRow(menuRowFactory.createMenuRow(this, PlayControlsRow.class)); 156 addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class)); 157 addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class)); 158 addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class)); 159 mMenuView.setMenuRows(mMenuRows); 160 mAutoHideScheduler = new AutoHideScheduler(context, () -> hide(true)); 161 } 162 163 /** 164 * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready 165 * or not available any more. 166 */ setChannelTuner(ChannelTuner channelTuner)167 public void setChannelTuner(ChannelTuner channelTuner) { 168 mMenuUpdater.setChannelTuner(channelTuner); 169 } 170 addMenuRow(MenuRow row)171 private void addMenuRow(MenuRow row) { 172 if (row != null) { 173 mMenuRows.add(row); 174 } 175 } 176 177 /** Call this method to end the lifetime of the menu. */ release()178 public void release() { 179 mMenuUpdater.release(); 180 for (MenuRow row : mMenuRows) { 181 row.release(); 182 } 183 mAutoHideScheduler.cancel(); 184 } 185 186 /** Preloads the item view used for the menu. */ preloadItemViews()187 public void preloadItemViews() { 188 HorizontalGridView fakeParent = new HorizontalGridView(mContext); 189 for (int id : PRELOAD_VIEW_IDS.keySet()) { 190 ViewCache.getInstance().putView(mContext, id, fakeParent, PRELOAD_VIEW_IDS.get(id)); 191 } 192 } 193 194 /** 195 * Shows the main menu. 196 * 197 * @param reason A reason why this is called. See {@link MenuShowReason} 198 */ show(@enuShowReason int reason)199 public void show(@MenuShowReason int reason) { 200 if (DEBUG) Log.d(TAG, "show reason:" + reason); 201 mTracker.sendShowMenu(); 202 mVisibleTimer.start(); 203 mTracker.sendScreenView(SCREEN_NAME); 204 if (mHideAnimator.isStarted()) { 205 mHideAnimator.end(); 206 } 207 if (mOnMenuVisibilityChangeListener != null) { 208 mOnMenuVisibilityChangeListener.onMenuVisibilityChange(true); 209 } 210 String rowIdToSelect = sRowIdListForReason.get(reason); 211 mMenuView.onShow( 212 reason, 213 rowIdToSelect, 214 mAnimationDisabledForTest 215 ? null 216 : () -> { 217 if (isActive()) { 218 mShowAnimator.start(); 219 } 220 }); 221 scheduleHide(); 222 } 223 224 /** Closes the menu. */ hide(boolean withAnimation)225 public void hide(boolean withAnimation) { 226 if (mShowAnimator.isStarted()) { 227 mShowAnimator.cancel(); 228 } 229 if (!isActive()) { 230 return; 231 } 232 if (mAnimationDisabledForTest) { 233 withAnimation = false; 234 } 235 mAutoHideScheduler.cancel(); 236 if (withAnimation) { 237 if (!mHideAnimator.isStarted()) { 238 mHideAnimator.start(); 239 } 240 } else if (mHideAnimator.isStarted()) { 241 // mMenuView.onHide() is called in AnimatorListener. 242 mHideAnimator.end(); 243 } else { 244 hideInternal(); 245 } 246 } 247 hideInternal()248 private void hideInternal() { 249 mMenuView.onHide(); 250 mTracker.sendHideMenu(mVisibleTimer.reset()); 251 if (mOnMenuVisibilityChangeListener != null) { 252 mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); 253 } 254 } 255 256 /** Schedules to hide the menu in some seconds. */ scheduleHide()257 public void scheduleHide() { 258 mAutoHideScheduler.schedule(mShowDurationMillis); 259 } 260 261 /** 262 * Called when the caller wants the main menu to be kept visible or not. If {@code keepVisible} 263 * is set to {@code true}, the hide schedule doesn't close the main menu, but calling {@link 264 * #hide} still hides it. If {@code keepVisible} is set to {@code false}, the hide schedule 265 * works as usual. 266 */ setKeepVisible(boolean keepVisible)267 public void setKeepVisible(boolean keepVisible) { 268 mKeepVisible = keepVisible; 269 if (mKeepVisible) { 270 mAutoHideScheduler.cancel(); 271 } else if (isActive()) { 272 scheduleHide(); 273 } 274 } 275 276 @VisibleForTesting isHideScheduled()277 boolean isHideScheduled() { 278 return mAutoHideScheduler.isScheduled(); 279 } 280 281 /** Returns {@code true} if the menu is open and not hiding. */ isActive()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 */ update()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 */ update(String rowId)301 public boolean update(String rowId) { 302 if (DEBUG) Log.d(TAG, "update main menu"); 303 return mMenuView.update(rowId, isActive()); 304 } 305 306 /** This method is called when channels are changed. */ onRecentChannelsChanged()307 public void onRecentChannelsChanged() { 308 if (DEBUG) Log.d(TAG, "onRecentChannelsChanged"); 309 for (MenuRow row : mMenuRows) { 310 row.onRecentChannelsChanged(); 311 } 312 } 313 314 /** This method is called when the stream information is changed. */ onStreamInfoChanged()315 public void onStreamInfoChanged() { 316 if (DEBUG) Log.d(TAG, "update options row in main menu"); 317 mMenuUpdater.onStreamInfoChanged(); 318 } 319 320 @Override onAccessibilityStateChanged(boolean enabled)321 public void onAccessibilityStateChanged(boolean enabled) { 322 mAutoHideScheduler.onAccessibilityStateChanged(enabled); 323 } 324 325 @VisibleForTesting disableAnimationForTest()326 void disableAnimationForTest() { 327 if (!CommonUtils.isRunningInTest()) { 328 throw new RuntimeException("Animation may only be enabled/disabled during tests."); 329 } 330 mAnimationDisabledForTest = true; 331 } 332 333 /** A listener which receives the notification when the menu is visible/invisible. */ 334 public abstract static class OnMenuVisibilityChangeListener { 335 /** Called when the menu becomes visible/invisible. */ onMenuVisibilityChange(boolean visible)336 public abstract void onMenuVisibilityChange(boolean visible); 337 } 338 } 339