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 android.support.v17.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 : new Runnable() { 217 @Override 218 public void run() { 219 if (isActive()) { 220 mShowAnimator.start(); 221 } 222 } 223 }); 224 scheduleHide(); 225 } 226 227 /** Closes the menu. */ hide(boolean withAnimation)228 public void hide(boolean withAnimation) { 229 if (mShowAnimator.isStarted()) { 230 mShowAnimator.cancel(); 231 } 232 if (!isActive()) { 233 return; 234 } 235 if (mAnimationDisabledForTest) { 236 withAnimation = false; 237 } 238 mAutoHideScheduler.cancel(); 239 if (withAnimation) { 240 if (!mHideAnimator.isStarted()) { 241 mHideAnimator.start(); 242 } 243 } else if (mHideAnimator.isStarted()) { 244 // mMenuView.onHide() is called in AnimatorListener. 245 mHideAnimator.end(); 246 } else { 247 hideInternal(); 248 } 249 } 250 hideInternal()251 private void hideInternal() { 252 mMenuView.onHide(); 253 mTracker.sendHideMenu(mVisibleTimer.reset()); 254 if (mOnMenuVisibilityChangeListener != null) { 255 mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); 256 } 257 } 258 259 /** Schedules to hide the menu in some seconds. */ scheduleHide()260 public void scheduleHide() { 261 mAutoHideScheduler.schedule(mShowDurationMillis); 262 } 263 264 /** 265 * Called when the caller wants the main menu to be kept visible or not. If {@code keepVisible} 266 * is set to {@code true}, the hide schedule doesn't close the main menu, but calling {@link 267 * #hide} still hides it. If {@code keepVisible} is set to {@code false}, the hide schedule 268 * works as usual. 269 */ setKeepVisible(boolean keepVisible)270 public void setKeepVisible(boolean keepVisible) { 271 mKeepVisible = keepVisible; 272 if (mKeepVisible) { 273 mAutoHideScheduler.cancel(); 274 } else if (isActive()) { 275 scheduleHide(); 276 } 277 } 278 279 @VisibleForTesting isHideScheduled()280 boolean isHideScheduled() { 281 return mAutoHideScheduler.isScheduled(); 282 } 283 284 /** Returns {@code true} if the menu is open and not hiding. */ isActive()285 public boolean isActive() { 286 return mMenuView.isVisible() && !mHideAnimator.isStarted(); 287 } 288 289 /** 290 * Updates menu contents. 291 * 292 * <p>Returns <@code true> if the contents have been changed, otherwise {@code false}. 293 */ update()294 public boolean update() { 295 if (DEBUG) Log.d(TAG, "update main menu"); 296 return mMenuView.update(isActive()); 297 } 298 299 /** 300 * Updates the menu row. 301 * 302 * <p>Returns <@code true> if the contents have been changed, otherwise {@code false}. 303 */ update(String rowId)304 public boolean update(String rowId) { 305 if (DEBUG) Log.d(TAG, "update main menu"); 306 return mMenuView.update(rowId, isActive()); 307 } 308 309 /** This method is called when channels are changed. */ onRecentChannelsChanged()310 public void onRecentChannelsChanged() { 311 if (DEBUG) Log.d(TAG, "onRecentChannelsChanged"); 312 for (MenuRow row : mMenuRows) { 313 row.onRecentChannelsChanged(); 314 } 315 } 316 317 /** This method is called when the stream information is changed. */ onStreamInfoChanged()318 public void onStreamInfoChanged() { 319 if (DEBUG) Log.d(TAG, "update options row in main menu"); 320 mMenuUpdater.onStreamInfoChanged(); 321 } 322 323 @Override onAccessibilityStateChanged(boolean enabled)324 public void onAccessibilityStateChanged(boolean enabled) { 325 mAutoHideScheduler.onAccessibilityStateChanged(enabled); 326 } 327 328 @VisibleForTesting disableAnimationForTest()329 void disableAnimationForTest() { 330 if (!CommonUtils.isRunningInTest()) { 331 throw new RuntimeException("Animation may only be enabled/disabled during tests."); 332 } 333 mAnimationDisabledForTest = true; 334 } 335 336 /** A listener which receives the notification when the menu is visible/invisible. */ 337 public abstract static class OnMenuVisibilityChangeListener { 338 /** Called when the menu becomes visible/invisible. */ onMenuVisibilityChange(boolean visible)339 public abstract void onMenuVisibilityChange(boolean visible); 340 } 341 } 342