blob: 0687441ebaceb852cbbfd0d8a224afeeb7c96953 [file] [log] [blame]
/*
* 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);
}
}