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