/* * 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.content.Context; import android.content.res.Resources; import android.text.format.DateFormat; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TimeShiftManager.TimeShiftActionId; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.menu.Menu.MenuShowReason; import com.android.tv.ui.TunableTvView; import com.android.tv.util.Utils; public class PlayControlsRowView extends MenuRowView { private static final int NORMAL_WIDTH_MAX_BUTTON_COUNT = 5; // Dimensions private final int mTimeIndicatorLeftMargin; private final int mTimeTextLeftMargin; private final int mTimelineWidth; // Views private View mBackgroundView; private View mTimeIndicator; private TextView mTimeText; private View mProgressEmptyBefore; private View mProgressWatched; private View mProgressBuffered; private View mProgressEmptyAfter; private View mControlBar; private PlayControlsButton mJumpPreviousButton; private PlayControlsButton mRewindButton; private PlayControlsButton mPlayPauseButton; private PlayControlsButton mFastForwardButton; private PlayControlsButton mJumpNextButton; private PlayControlsButton mRecordButton; private TextView mProgramStartTimeText; private TextView mProgramEndTimeText; private View mUnavailableMessageText; private TunableTvView mTvView; private TimeShiftManager mTimeShiftManager; private final DvrDataManager mDvrDataManager; private final DvrManager mDvrManager; private final MainActivity mMainActivity; private final java.text.DateFormat mTimeFormat; private long mProgramStartTimeMs; private long mProgramEndTimeMs; private boolean mUseCompactLayout; private final int mNormalButtonMargin; private final int mCompactButtonMargin; private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { } @Override public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { } @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { Channel currentChannel = mMainActivity.getCurrentChannel(); if (currentChannel != null && isShown()) { for (ScheduledRecording schedule : scheduledRecordings) { if (schedule.getChannelId() == currentChannel.getId()) { updateRecordButton(); break; } } } } }; public PlayControlsRowView(Context context) { this(context, null); } public PlayControlsRowView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Resources res = context.getResources(); mTimeIndicatorLeftMargin = - res.getDimensionPixelSize(R.dimen.play_controls_time_indicator_width) / 2; mTimeTextLeftMargin = - res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2; mTimelineWidth = res.getDimensionPixelSize(R.dimen.play_controls_width); mTimeFormat = DateFormat.getTimeFormat(context); mNormalButtonMargin = res.getDimensionPixelSize(R.dimen.play_controls_button_normal_margin); mCompactButtonMargin = res.getDimensionPixelSize(R.dimen.play_controls_button_compact_margin); if (CommonFeatures.DVR.isEnabled(context)) { mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); mDvrManager = TvApplication.getSingletons(context).getDvrManager(); } else { mDvrDataManager = null; mDvrManager = null; } mMainActivity = (MainActivity) context; } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); if (mDvrDataManager != null) { mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); if (!mDvrDataManager.isDvrScheduleLoadFinished()) { mDvrDataManager.addDvrScheduleLoadFinishedListener( new OnDvrScheduleLoadFinishedListener() { @Override public void onDvrScheduleLoadFinished() { mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); if (isShown()) { updateRecordButton(); } } }); } } } @Override protected int getContentsViewId() { return R.id.play_controls; } @Override protected void onFinishInflate() { super.onFinishInflate(); // Clip the ViewGroup(body) to the rounded rectangle of outline. findViewById(R.id.body).setClipToOutline(true); mBackgroundView = findViewById(R.id.background); mTimeIndicator = findViewById(R.id.time_indicator); mTimeText = (TextView) findViewById(R.id.time_text); mProgressEmptyBefore = findViewById(R.id.timeline_bg_start); mProgressWatched = findViewById(R.id.watched); mProgressBuffered = findViewById(R.id.buffered); mProgressEmptyAfter = findViewById(R.id.timeline_bg_end); mControlBar = findViewById(R.id.play_control_bar); mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous); mRewindButton = (PlayControlsButton) findViewById(R.id.rewind); mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause); mFastForwardButton = (PlayControlsButton) findViewById(R.id.fast_forward); mJumpNextButton = (PlayControlsButton) findViewById(R.id.jump_next); mRecordButton = (PlayControlsButton) findViewById(R.id.record); mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time); mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time); mUnavailableMessageText = findViewById(R.id.unavailable_text); initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous, R.string.play_controls_description_skip_previous, null, new Runnable() { @Override public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToPrevious(); updateControls(); } } }); initializeButton(mRewindButton, R.drawable.lb_ic_fast_rewind, R.string.play_controls_description_fast_rewind, null, new Runnable() { @Override public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.rewind(); updateButtons(); } } }); initializeButton(mPlayPauseButton, R.drawable.lb_ic_play, R.string.play_controls_description_play_pause, null, new Runnable() { @Override public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.togglePlayPause(); updateButtons(); } } }); initializeButton(mFastForwardButton, R.drawable.lb_ic_fast_forward, R.string.play_controls_description_fast_forward, null, new Runnable() { @Override public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.fastForward(); updateButtons(); } } }); initializeButton(mJumpNextButton, R.drawable.lb_ic_skip_next, R.string.play_controls_description_skip_next, null, new Runnable() { @Override public void run() { if (mTimeShiftManager.isAvailable()) { mTimeShiftManager.jumpToNext(); updateControls(); } } }); int color = getResources().getColor(R.color.play_controls_recording_icon_color_on_focus, null); initializeButton(mRecordButton, R.drawable.ic_record_start, R.string .channels_item_record_start, color, new Runnable() { @Override public void run() { onRecordButtonClicked(); } }); } private boolean isCurrentChannelRecording() { Channel currentChannel = mMainActivity.getCurrentChannel(); return currentChannel != null && mDvrManager != null && mDvrManager.getCurrentRecording(currentChannel.getId()) != null; } private void onRecordButtonClicked() { boolean isRecording = isCurrentChannelRecording(); Channel currentChannel = mMainActivity.getCurrentChannel(); TvApplication.getSingletons(getContext()).getTracker().sendMenuClicked(isRecording ? R.string.channels_item_record_start : R.string.channels_item_record_stop); if (!isRecording) { if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) { Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel, Toast.LENGTH_SHORT).show(); } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, currentChannel.getInputId())) { Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager() .getCurrentProgram(currentChannel.getId()); if (program == null) { DvrUiHelper.showChannelRecordDurationOptions(mMainActivity, currentChannel); } else if (DvrUiHelper.handleCreateSchedule(mMainActivity, program)) { String msg = mMainActivity.getString(R.string.dvr_msg_current_program_scheduled, program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show(); } } } else if (currentChannel != null) { DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(), DvrStopRecordingFragment.REASON_USER_STOP, new HalfSizedDialogFragment.OnActionClickListener() { @Override public void onActionClick(long actionId) { if (actionId == DvrStopRecordingFragment.ACTION_STOP) { ScheduledRecording currentRecording = mDvrManager.getCurrentRecording( currentChannel.getId()); if (currentRecording != null) { mDvrManager.stopRecording(currentRecording); } } } }); } } private void initializeButton(PlayControlsButton button, int imageResId, int descriptionId, Integer focusedIconColor, Runnable clickAction) { button.setImageResId(imageResId); button.setAction(clickAction); if (focusedIconColor != null) { button.setFocusedIconColor(focusedIconColor); } button.findViewById(R.id.button) .setContentDescription(getResources().getString(descriptionId)); } @Override public void onBind(MenuRow row) { super.onBind(row); PlayControlsRow playControlsRow = (PlayControlsRow) row; mTvView = playControlsRow.getTvView(); mTimeShiftManager = playControlsRow.getTimeShiftManager(); mTimeShiftManager.setListener(new TimeShiftManager.Listener() { @Override public void onAvailabilityChanged() { updateMenuVisibility(); if (isShown()) { PlayControlsRowView.this.updateAll(); } } @Override public void onPlayStatusChanged(int status) { updateMenuVisibility(); if (mTimeShiftManager.isAvailable() && isShown()) { updateControls(); } } @Override public void onRecordTimeRangeChanged() { if (mTimeShiftManager.isAvailable() && isShown()) { updateControls(); } } @Override public void onCurrentPositionChanged() { if (mTimeShiftManager.isAvailable() && isShown()) { initializeTimeline(); updateControls(); } } @Override public void onProgramInfoChanged() { if (mTimeShiftManager.isAvailable() && isShown()) { initializeTimeline(); updateControls(); } } @Override public void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled) { // Move focus to the play/pause button when the PREVIOUS, NEXT, REWIND or // FAST_FORWARD button is clicked and the button becomes disabled. // No need to update the UI here because the UI will be updated by other callbacks. if (!enabled && ((actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS && mJumpPreviousButton.hasFocus()) || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND && mRewindButton.hasFocus()) || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD && mFastForwardButton.hasFocus()) || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT && mJumpNextButton.hasFocus()))) { mPlayPauseButton.requestFocus(); } } }); updateAll(); } private void initializeTimeline() { Program program = mTimeShiftManager.getProgramAt( mTimeShiftManager.getCurrentPositionMs()); mProgramStartTimeMs = program.getStartTimeUtcMillis(); mProgramEndTimeMs = program.getEndTimeUtcMillis(); SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs); } private void updateMenuVisibility() { boolean keepMenuVisible = mTimeShiftManager.isAvailable() && !mTimeShiftManager.isNormalPlaying(); getMenu().setKeepVisible(keepMenuVisible); } @Override public void onSelected(boolean showTitle) { super.onSelected(showTitle); updateControls(); postHideRippleAnimation(); } @Override public void initialize(@MenuShowReason int reason) { super.initialize(reason); switch (reason) { case Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) { setInitialFocusView(mJumpPreviousButton); } else { setInitialFocusView(mPlayPauseButton); } break; case Menu.REASON_PLAY_CONTROLS_REWIND: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)) { setInitialFocusView(mRewindButton); } else { setInitialFocusView(mPlayPauseButton); } break; case Menu.REASON_PLAY_CONTROLS_FAST_FORWARD: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)) { setInitialFocusView(mFastForwardButton); } else { setInitialFocusView(mPlayPauseButton); } break; case Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT: if (mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) { setInitialFocusView(mJumpNextButton); } else { setInitialFocusView(mPlayPauseButton); } break; case Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE: case Menu.REASON_PLAY_CONTROLS_PLAY: case Menu.REASON_PLAY_CONTROLS_PAUSE: default: setInitialFocusView(mPlayPauseButton); break; } postHideRippleAnimation(); } private void postHideRippleAnimation() { // Focus may be changed in another message if requestFocus is called in this message. // After the focus is actually changed, hideRippleAnimation should run // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted. post(new Runnable() { @Override public void run() { mJumpPreviousButton.hideRippleAnimation(); mRewindButton.hideRippleAnimation(); mPlayPauseButton.hideRippleAnimation(); mFastForwardButton.hideRippleAnimation(); mJumpNextButton.hideRippleAnimation(); } }); } @Override protected void onChildFocusChange(View v, boolean hasFocus) { super.onChildFocusChange(v, hasFocus); if ((v.getParent().equals(mRewindButton) || v.getParent().equals(mFastForwardButton)) && !hasFocus) { if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PLAYING) { mTimeShiftManager.play(); updateButtons(); } } } /** * Updates the view contents. It is called from the PlayControlsRow. */ public void update() { updateAll(); } private void updateAll() { if (mTimeShiftManager.isAvailable() && !mTvView.isScreenBlocked()) { setEnabled(true); initializeTimeline(); mBackgroundView.setEnabled(true); } else { setEnabled(false); mBackgroundView.setEnabled(false); } updateControls(); } private void updateControls() { updateTime(); updateProgress(); updateRecTimeText(); updateButtons(); updateRecordButton(); updateButtonMargin(); } private void updateTime() { if (isEnabled()) { mTimeText.setVisibility(View.VISIBLE); mTimeIndicator.setVisibility(View.VISIBLE); } else { mTimeText.setVisibility(View.INVISIBLE); mTimeIndicator.setVisibility(View.INVISIBLE); return; } long currentPositionMs = mTimeShiftManager.getCurrentPositionMs(); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams(); int currentTimePositionPixel = convertDurationToPixel(currentPositionMs - mProgramStartTimeMs); params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin; mTimeText.setLayoutParams(params); mTimeText.setText(getTimeString(currentPositionMs)); params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams(); params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin; mTimeIndicator.setLayoutParams(params); } private void updateProgress() { if (isEnabled()) { mProgressWatched.setVisibility(View.VISIBLE); mProgressBuffered.setVisibility(View.VISIBLE); mProgressEmptyAfter.setVisibility(View.VISIBLE); } else { mProgressWatched.setVisibility(View.INVISIBLE); mProgressBuffered.setVisibility(View.INVISIBLE); mProgressEmptyAfter.setVisibility(View.INVISIBLE); if (mProgramStartTimeMs < mProgramEndTimeMs) { layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs); } else { // Not initialized yet. layoutProgress(mProgressEmptyBefore, mTimelineWidth); } return; } long progressStartTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs())); long currentPlayingTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs())); long progressEndTimeMs = Math.min(mProgramEndTimeMs, Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs())); layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs); layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs); layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs); } private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) { layoutProgress(progress, Math.max(0, convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1); } private void layoutProgress(View progress, int width) { ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) progress.getLayoutParams(); params.width = width; progress.setLayoutParams(params); } private void updateRecTimeText() { if (isEnabled()) { mProgramStartTimeText.setVisibility(View.VISIBLE); mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs)); mProgramEndTimeText.setVisibility(View.VISIBLE); mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs)); } else { mProgramStartTimeText.setVisibility(View.GONE); mProgramEndTimeText.setVisibility(View.GONE); } } private void updateButtons() { if (isEnabled()) { mControlBar.setVisibility(View.VISIBLE); mUnavailableMessageText.setVisibility(View.GONE); } else { mControlBar.setVisibility(View.INVISIBLE); mUnavailableMessageText.setVisibility(View.VISIBLE); return; } if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PAUSED) { mPlayPauseButton.setImageResId(R.drawable.lb_ic_play); mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY)); } else { mPlayPauseButton.setImageResId(R.drawable.lb_ic_pause); mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE)); } mJumpPreviousButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)); mRewindButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)); mFastForwardButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)); mJumpNextButton.setEnabled(mTimeShiftManager.isActionEnabled( TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)); mJumpPreviousButton.setVisibility(VISIBLE); mJumpNextButton.setVisibility(VISIBLE); updateButtonMargin(); PlayControlsButton button; if (mTimeShiftManager.getPlayDirection() == TimeShiftManager.PLAY_DIRECTION_FORWARD) { mRewindButton.setLabel(null); button = mFastForwardButton; } else { mFastForwardButton.setLabel(null); button = mRewindButton; } if (mTimeShiftManager.getDisplayedPlaySpeed() == TimeShiftManager.PLAY_SPEED_1X) { button.setLabel(null); } else { button.setLabel(getResources().getString(R.string.play_controls_speed, mTimeShiftManager.getDisplayedPlaySpeed())); } } private void updateRecordButton() { if (!(mDvrManager != null && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) { mRecordButton.setVisibility(View.GONE); updateButtonMargin(); return; } mRecordButton.setVisibility(View.VISIBLE); updateButtonMargin(); if (isCurrentChannelRecording()) { mRecordButton.setImageResId(R.drawable.ic_record_stop); } else { mRecordButton.setImageResId(R.drawable.ic_record_start); } } private void updateButtonMargin() { int numOfVisibleButtons = (mJumpPreviousButton.getVisibility() == View.VISIBLE ? 1 : 0) + (mRewindButton.getVisibility() == View.VISIBLE ? 1 : 0) + (mPlayPauseButton.getVisibility() == View.VISIBLE ? 1 : 0) + (mFastForwardButton.getVisibility() == View.VISIBLE ? 1 : 0) + (mJumpNextButton.getVisibility() == View.VISIBLE ? 1 : 0) + (mRecordButton.getVisibility() == View.VISIBLE ? 1 : 0); boolean useCompactLayout = numOfVisibleButtons > NORMAL_WIDTH_MAX_BUTTON_COUNT; if (mUseCompactLayout == useCompactLayout) { return; } mUseCompactLayout = useCompactLayout; int margin = mUseCompactLayout ? mCompactButtonMargin : mNormalButtonMargin; updateButtonMargin(mJumpPreviousButton, margin); updateButtonMargin(mRewindButton, margin); updateButtonMargin(mPlayPauseButton, margin); updateButtonMargin(mFastForwardButton, margin); updateButtonMargin(mJumpNextButton, margin); updateButtonMargin(mRecordButton, margin); } private void updateButtonMargin(PlayControlsButton button, int margin) { MarginLayoutParams params = (MarginLayoutParams) button.getLayoutParams(); params.setMargins(margin, 0, margin, 0); button.setLayoutParams(params); } private String getTimeString(long timeMs) { return mTimeFormat.format(timeMs); } private int convertDurationToPixel(long duration) { if (mProgramEndTimeMs <= mProgramStartTimeMs) { return 0; } return (int) (duration * mTimelineWidth / (mProgramEndTimeMs - mProgramStartTimeMs)); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mDvrDataManager != null) { mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); } } }