/*
|
* Copyright (C) 2016 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.systemui.pip.tv;
|
|
import android.app.PendingIntent.CanceledException;
|
import android.app.RemoteAction;
|
import android.content.Context;
|
import android.graphics.Color;
|
import android.media.session.MediaController;
|
import android.media.session.PlaybackState;
|
import android.os.Handler;
|
import android.util.AttributeSet;
|
import android.util.Log;
|
import android.view.Gravity;
|
import android.view.LayoutInflater;
|
import android.view.View;
|
import android.widget.LinearLayout;
|
|
import com.android.systemui.R;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
|
|
/**
|
* A view containing PIP controls including fullscreen, close, and media controls.
|
*/
|
public class PipControlsView extends LinearLayout {
|
|
private static final String TAG = PipControlsView.class.getSimpleName();
|
|
private static final float DISABLED_ACTION_ALPHA = 0.54f;
|
|
/**
|
* An interface to listen user action.
|
*/
|
public abstract static interface Listener {
|
/**
|
* Called when an user clicks close PIP button.
|
*/
|
public abstract void onClosed();
|
};
|
|
private MediaController mMediaController;
|
|
private final PipManager mPipManager = PipManager.getInstance();
|
private final LayoutInflater mLayoutInflater;
|
private final Handler mHandler;
|
private Listener mListener;
|
|
private PipControlButtonView mFullButtonView;
|
private PipControlButtonView mCloseButtonView;
|
private PipControlButtonView mPlayPauseButtonView;
|
private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>();
|
private List<RemoteAction> mCustomActions = new ArrayList<>();
|
|
private PipControlButtonView mFocusedChild;
|
|
private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
|
@Override
|
public void onPlaybackStateChanged(PlaybackState state) {
|
updateUserActions();
|
}
|
};
|
|
private final PipManager.MediaListener mPipMediaListener = new PipManager.MediaListener() {
|
@Override
|
public void onMediaControllerChanged() {
|
updateMediaController();
|
}
|
};
|
|
private final OnFocusChangeListener mFocusChangeListener = new OnFocusChangeListener() {
|
@Override
|
public void onFocusChange(View view, boolean hasFocus) {
|
if (hasFocus) {
|
mFocusedChild = (PipControlButtonView) view;
|
} else if (mFocusedChild == view) {
|
mFocusedChild = null;
|
}
|
}
|
};
|
|
public PipControlsView(Context context) {
|
this(context, null, 0, 0);
|
}
|
|
public PipControlsView(Context context, AttributeSet attrs) {
|
this(context, attrs, 0, 0);
|
}
|
|
public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) {
|
this(context, attrs, defStyleAttr, 0);
|
}
|
|
public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
super(context, attrs, defStyleAttr, defStyleRes);
|
mLayoutInflater = (LayoutInflater) getContext()
|
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
mLayoutInflater.inflate(R.layout.tv_pip_controls, this);
|
mHandler = new Handler();
|
|
setOrientation(LinearLayout.HORIZONTAL);
|
setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
|
}
|
|
@Override
|
public void onFinishInflate() {
|
super.onFinishInflate();
|
|
mFullButtonView = findViewById(R.id.full_button);
|
mFullButtonView.setOnFocusChangeListener(mFocusChangeListener);
|
mFullButtonView.setOnClickListener(new View.OnClickListener() {
|
@Override
|
public void onClick(View v) {
|
mPipManager.movePipToFullscreen();
|
}
|
});
|
|
mCloseButtonView = findViewById(R.id.close_button);
|
mCloseButtonView.setOnFocusChangeListener(mFocusChangeListener);
|
mCloseButtonView.setOnClickListener(new View.OnClickListener() {
|
@Override
|
public void onClick(View v) {
|
mPipManager.closePip();
|
if (mListener != null) {
|
mListener.onClosed();
|
}
|
}
|
});
|
|
mPlayPauseButtonView = findViewById(R.id.play_pause_button);
|
mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener);
|
mPlayPauseButtonView.setOnClickListener(new View.OnClickListener() {
|
@Override
|
public void onClick(View v) {
|
if (mMediaController == null || mMediaController.getPlaybackState() == null) {
|
return;
|
}
|
long actions = mMediaController.getPlaybackState().getActions();
|
int state = mMediaController.getPlaybackState().getState();
|
if (mPipManager.getPlaybackState() == PipManager.PLAYBACK_STATE_PAUSED) {
|
mMediaController.getTransportControls().play();
|
} else if (mPipManager.getPlaybackState() == PipManager.PLAYBACK_STATE_PLAYING) {
|
mMediaController.getTransportControls().pause();
|
}
|
// View will be updated later in {@link mMediaControllerCallback}
|
}
|
});
|
}
|
|
@Override
|
public void onAttachedToWindow() {
|
super.onAttachedToWindow();
|
updateMediaController();
|
mPipManager.addMediaListener(mPipMediaListener);
|
}
|
|
@Override
|
public void onDetachedFromWindow() {
|
super.onDetachedFromWindow();
|
mPipManager.removeMediaListener(mPipMediaListener);
|
if (mMediaController != null) {
|
mMediaController.unregisterCallback(mMediaControllerCallback);
|
}
|
}
|
|
private void updateMediaController() {
|
MediaController newController = mPipManager.getMediaController();
|
if (mMediaController == newController) {
|
return;
|
}
|
if (mMediaController != null) {
|
mMediaController.unregisterCallback(mMediaControllerCallback);
|
}
|
mMediaController = newController;
|
if (mMediaController != null) {
|
mMediaController.registerCallback(mMediaControllerCallback);
|
}
|
updateUserActions();
|
}
|
|
/**
|
* Updates the actions for the PIP. If there are no custom actions, then the media session
|
* actions are shown.
|
*/
|
private void updateUserActions() {
|
if (!mCustomActions.isEmpty()) {
|
// Ensure we have as many buttons as actions
|
while (mCustomButtonViews.size() < mCustomActions.size()) {
|
PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate(
|
R.layout.tv_pip_custom_control, this, false);
|
addView(buttonView);
|
mCustomButtonViews.add(buttonView);
|
}
|
|
// Update the visibility of all views
|
for (int i = 0; i < mCustomButtonViews.size(); i++) {
|
mCustomButtonViews.get(i).setVisibility(i < mCustomActions.size()
|
? View.VISIBLE
|
: View.GONE);
|
}
|
|
// Update the state and visibility of the action buttons, and hide the rest
|
for (int i = 0; i < mCustomActions.size(); i++) {
|
final RemoteAction action = mCustomActions.get(i);
|
PipControlButtonView actionView = mCustomButtonViews.get(i);
|
|
// TODO: Check if the action drawable has changed before we reload it
|
action.getIcon().loadDrawableAsync(getContext(), d -> {
|
d.setTint(Color.WHITE);
|
actionView.setImageDrawable(d);
|
}, mHandler);
|
actionView.setText(action.getContentDescription());
|
if (action.isEnabled()) {
|
actionView.setOnClickListener(v -> {
|
try {
|
action.getActionIntent().send();
|
} catch (CanceledException e) {
|
Log.w(TAG, "Failed to send action", e);
|
}
|
});
|
}
|
actionView.setEnabled(action.isEnabled());
|
actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
|
}
|
|
// Hide the media session buttons
|
mPlayPauseButtonView.setVisibility(View.GONE);
|
} else {
|
int state = mPipManager.getPlaybackState();
|
if (state == PipManager.PLAYBACK_STATE_UNAVAILABLE) {
|
mPlayPauseButtonView.setVisibility(View.GONE);
|
} else {
|
mPlayPauseButtonView.setVisibility(View.VISIBLE);
|
if (state == PipManager.PLAYBACK_STATE_PLAYING) {
|
mPlayPauseButtonView.setImageResource(R.drawable.ic_pause_white);
|
mPlayPauseButtonView.setText(R.string.pip_pause);
|
} else {
|
mPlayPauseButtonView.setImageResource(R.drawable.ic_play_arrow_white);
|
mPlayPauseButtonView.setText(R.string.pip_play);
|
}
|
}
|
|
// Hide all the custom action buttons
|
for (int i = 0; i < mCustomButtonViews.size(); i++) {
|
mCustomButtonViews.get(i).setVisibility(View.GONE);
|
}
|
}
|
}
|
|
/**
|
* Resets to initial state.
|
*/
|
public void reset() {
|
mFullButtonView.reset();
|
mCloseButtonView.reset();
|
mPlayPauseButtonView.reset();
|
mFullButtonView.requestFocus();
|
for (int i = 0; i < mCustomButtonViews.size(); i++) {
|
mCustomButtonViews.get(i).reset();
|
}
|
}
|
|
/**
|
* Sets the {@link Listener} to listen user actions.
|
*/
|
public void setListener(Listener listener) {
|
mListener = listener;
|
}
|
|
/**
|
* Updates the set of activity-defined actions.
|
*/
|
public void setActions(List<RemoteAction> actions) {
|
mCustomActions.clear();
|
mCustomActions.addAll(actions);
|
updateUserActions();
|
}
|
|
/**
|
* Returns the focused control button view to animate focused button.
|
*/
|
PipControlButtonView getFocusedButton() {
|
return mFocusedChild;
|
}
|
}
|