| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.menu; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.AnimatorListenerAdapter; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.VisibleForTesting; |
| import androidx.leanback.widget.HorizontalGridView; |
| import android.util.Log; |
| import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; |
| import com.android.tv.ChannelTuner; |
| import com.android.tv.R; |
| import com.android.tv.TvOptionsManager; |
| import com.android.tv.TvSingletons; |
| import com.android.tv.analytics.Tracker; |
| import com.android.tv.common.util.CommonUtils; |
| import com.android.tv.common.util.DurationTimer; |
| import com.android.tv.menu.MenuRowFactory.PartnerRow; |
| import com.android.tv.menu.MenuRowFactory.TvOptionsRow; |
| import com.android.tv.ui.TunableTvView; |
| import com.android.tv.ui.hideable.AutoHideScheduler; |
| import com.android.tv.util.ViewCache; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** A class which controls the menu. */ |
| public class Menu implements AccessibilityStateChangeListener { |
| private static final String TAG = "Menu"; |
| private static final boolean DEBUG = false; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({ |
| REASON_NONE, |
| REASON_GUIDE, |
| REASON_PLAY_CONTROLS_PLAY, |
| REASON_PLAY_CONTROLS_PAUSE, |
| REASON_PLAY_CONTROLS_PLAY_PAUSE, |
| REASON_PLAY_CONTROLS_REWIND, |
| REASON_PLAY_CONTROLS_FAST_FORWARD, |
| REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS, |
| REASON_PLAY_CONTROLS_JUMP_TO_NEXT |
| }) |
| public @interface MenuShowReason {} |
| |
| public static final int REASON_NONE = 0; |
| public static final int REASON_GUIDE = 1; |
| public static final int REASON_PLAY_CONTROLS_PLAY = 2; |
| public static final int REASON_PLAY_CONTROLS_PAUSE = 3; |
| public static final int REASON_PLAY_CONTROLS_PLAY_PAUSE = 4; |
| public static final int REASON_PLAY_CONTROLS_REWIND = 5; |
| public static final int REASON_PLAY_CONTROLS_FAST_FORWARD = 6; |
| public static final int REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS = 7; |
| public static final int REASON_PLAY_CONTROLS_JUMP_TO_NEXT = 8; |
| |
| private static final List<String> sRowIdListForReason = new ArrayList<>(); |
| |
| static { |
| sRowIdListForReason.add(null); // REASON_NONE |
| sRowIdListForReason.add(ChannelsRow.ID); // REASON_GUIDE |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PAUSE |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_PLAY_PAUSE |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_REWIND |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_FAST_FORWARD |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS |
| sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT |
| } |
| |
| private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>(); |
| |
| static { |
| PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1); |
| PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1); |
| PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1); |
| 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); |
| PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7); |
| } |
| |
| private static final String SCREEN_NAME = "Menu"; |
| |
| private final Context mContext; |
| private final IMenuView mMenuView; |
| private final Tracker mTracker; |
| private final DurationTimer mVisibleTimer = new DurationTimer(); |
| private final long mShowDurationMillis; |
| private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener; |
| private final AutoHideScheduler mAutoHideScheduler; |
| |
| private final MenuUpdater mMenuUpdater; |
| private final List<MenuRow> mMenuRows = new ArrayList<>(); |
| private final Animator mShowAnimator; |
| private final Animator mHideAnimator; |
| |
| private boolean mKeepVisible; |
| private boolean mAnimationDisabledForTest; |
| |
| @VisibleForTesting |
| Menu( |
| Context context, |
| IMenuView menuView, |
| MenuRowFactory menuRowFactory, |
| OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { |
| this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener); |
| } |
| |
| public Menu( |
| Context context, |
| TunableTvView tvView, |
| TvOptionsManager optionsManager, |
| IMenuView menuView, |
| MenuRowFactory menuRowFactory, |
| OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) { |
| mContext = context; |
| mMenuView = menuView; |
| mTracker = TvSingletons.getSingletons(context).getTracker(); |
| mMenuUpdater = new MenuUpdater(this, tvView, optionsManager); |
| Resources res = context.getResources(); |
| mShowDurationMillis = res.getInteger(R.integer.menu_show_duration); |
| mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener; |
| mShowAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_enter); |
| mShowAnimator.setTarget(mMenuView); |
| mHideAnimator = AnimatorInflater.loadAnimator(context, R.animator.menu_exit); |
| mHideAnimator.addListener( |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| hideInternal(); |
| } |
| }); |
| mHideAnimator.setTarget(mMenuView); |
| // Build menu rows |
| addMenuRow(menuRowFactory.createMenuRow(this, PlayControlsRow.class)); |
| addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class)); |
| addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class)); |
| addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class)); |
| mMenuView.setMenuRows(mMenuRows); |
| mAutoHideScheduler = new AutoHideScheduler(context, () -> hide(true)); |
| } |
| |
| /** |
| * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready |
| * or not available any more. |
| */ |
| public void setChannelTuner(ChannelTuner channelTuner) { |
| mMenuUpdater.setChannelTuner(channelTuner); |
| } |
| |
| private void addMenuRow(MenuRow row) { |
| if (row != null) { |
| mMenuRows.add(row); |
| } |
| } |
| |
| /** Call this method to end the lifetime of the menu. */ |
| public void release() { |
| mMenuUpdater.release(); |
| for (MenuRow row : mMenuRows) { |
| row.release(); |
| } |
| mAutoHideScheduler.cancel(); |
| } |
| |
| /** Preloads the item view used for the menu. */ |
| public void preloadItemViews() { |
| HorizontalGridView fakeParent = new HorizontalGridView(mContext); |
| for (int id : PRELOAD_VIEW_IDS.keySet()) { |
| ViewCache.getInstance().putView(mContext, id, fakeParent, PRELOAD_VIEW_IDS.get(id)); |
| } |
| } |
| |
| /** |
| * Shows the main menu. |
| * |
| * @param reason A reason why this is called. See {@link MenuShowReason} |
| */ |
| public void show(@MenuShowReason int reason) { |
| if (DEBUG) Log.d(TAG, "show reason:" + reason); |
| mTracker.sendShowMenu(); |
| mVisibleTimer.start(); |
| mTracker.sendScreenView(SCREEN_NAME); |
| if (mHideAnimator.isStarted()) { |
| mHideAnimator.end(); |
| } |
| if (mOnMenuVisibilityChangeListener != null) { |
| mOnMenuVisibilityChangeListener.onMenuVisibilityChange(true); |
| } |
| String rowIdToSelect = sRowIdListForReason.get(reason); |
| mMenuView.onShow( |
| reason, |
| rowIdToSelect, |
| mAnimationDisabledForTest |
| ? null |
| : () -> { |
| if (isActive()) { |
| mShowAnimator.start(); |
| } |
| }); |
| scheduleHide(); |
| } |
| |
| /** Closes the menu. */ |
| public void hide(boolean withAnimation) { |
| if (mShowAnimator.isStarted()) { |
| mShowAnimator.cancel(); |
| } |
| if (!isActive()) { |
| return; |
| } |
| if (mAnimationDisabledForTest) { |
| withAnimation = false; |
| } |
| mAutoHideScheduler.cancel(); |
| if (withAnimation) { |
| if (!mHideAnimator.isStarted()) { |
| mHideAnimator.start(); |
| } |
| } else if (mHideAnimator.isStarted()) { |
| // mMenuView.onHide() is called in AnimatorListener. |
| mHideAnimator.end(); |
| } else { |
| hideInternal(); |
| } |
| } |
| |
| private void hideInternal() { |
| mMenuView.onHide(); |
| mTracker.sendHideMenu(mVisibleTimer.reset()); |
| if (mOnMenuVisibilityChangeListener != null) { |
| mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); |
| } |
| } |
| |
| /** Schedules to hide the menu in some seconds. */ |
| public void scheduleHide() { |
| mAutoHideScheduler.schedule(mShowDurationMillis); |
| } |
| |
| /** |
| * Called when the caller wants the main menu to be kept visible or not. If {@code keepVisible} |
| * is set to {@code true}, the hide schedule doesn't close the main menu, but calling {@link |
| * #hide} still hides it. If {@code keepVisible} is set to {@code false}, the hide schedule |
| * works as usual. |
| */ |
| public void setKeepVisible(boolean keepVisible) { |
| mKeepVisible = keepVisible; |
| if (mKeepVisible) { |
| mAutoHideScheduler.cancel(); |
| } else if (isActive()) { |
| scheduleHide(); |
| } |
| } |
| |
| @VisibleForTesting |
| boolean isHideScheduled() { |
| return mAutoHideScheduler.isScheduled(); |
| } |
| |
| /** Returns {@code true} if the menu is open and not hiding. */ |
| public boolean isActive() { |
| return mMenuView.isVisible() && !mHideAnimator.isStarted(); |
| } |
| |
| /** |
| * Updates menu contents. |
| * |
| * <p>Returns <@code true> if the contents have been changed, otherwise {@code false}. |
| */ |
| public boolean update() { |
| if (DEBUG) Log.d(TAG, "update main menu"); |
| return mMenuView.update(isActive()); |
| } |
| |
| /** |
| * Updates the menu row. |
| * |
| * <p>Returns <@code true> if the contents have been changed, otherwise {@code false}. |
| */ |
| public boolean update(String rowId) { |
| if (DEBUG) Log.d(TAG, "update main menu"); |
| return mMenuView.update(rowId, isActive()); |
| } |
| |
| /** This method is called when channels are changed. */ |
| public void onRecentChannelsChanged() { |
| if (DEBUG) Log.d(TAG, "onRecentChannelsChanged"); |
| for (MenuRow row : mMenuRows) { |
| row.onRecentChannelsChanged(); |
| } |
| } |
| |
| /** This method is called when the stream information is changed. */ |
| public void onStreamInfoChanged() { |
| if (DEBUG) Log.d(TAG, "update options row in main menu"); |
| mMenuUpdater.onStreamInfoChanged(); |
| } |
| |
| @Override |
| public void onAccessibilityStateChanged(boolean enabled) { |
| mAutoHideScheduler.onAccessibilityStateChanged(enabled); |
| } |
| |
| @VisibleForTesting |
| void disableAnimationForTest() { |
| if (!CommonUtils.isRunningInTest()) { |
| throw new RuntimeException("Animation may only be enabled/disabled during tests."); |
| } |
| mAnimationDisabledForTest = true; |
| } |
| |
| /** A listener which receives the notification when the menu is visible/invisible. */ |
| public abstract static class OnMenuVisibilityChangeListener { |
| /** Called when the menu becomes visible/invisible. */ |
| public abstract void onMenuVisibilityChange(boolean visible); |
| } |
| } |