1 /* 2 * Copyright (C) 2016 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.systemui.pip.tv; 18 19 import android.app.ActivityManager; 20 import android.app.PendingIntent.CanceledException; 21 import android.app.RemoteAction; 22 import android.content.Context; 23 import android.graphics.Color; 24 import android.media.session.MediaController; 25 import android.media.session.PlaybackState; 26 import android.os.Handler; 27 import android.os.RemoteException; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.Gravity; 31 import android.view.LayoutInflater; 32 import android.widget.ImageView; 33 import android.widget.LinearLayout; 34 import android.util.AttributeSet; 35 36 import com.android.systemui.R; 37 38 import static android.media.session.PlaybackState.ACTION_PAUSE; 39 import static android.media.session.PlaybackState.ACTION_PLAY; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 44 45 /** 46 * A view containing PIP controls including fullscreen, close, and media controls. 47 */ 48 public class PipControlsView extends LinearLayout { 49 50 private static final String TAG = PipControlsView.class.getSimpleName(); 51 52 private static final float DISABLED_ACTION_ALPHA = 0.54f; 53 54 /** 55 * An interface to listen user action. 56 */ 57 public abstract static interface Listener { 58 /** 59 * Called when an user clicks close PIP button. 60 */ onClosed()61 public abstract void onClosed(); 62 }; 63 64 private MediaController mMediaController; 65 66 private final PipManager mPipManager = PipManager.getInstance(); 67 private final LayoutInflater mLayoutInflater; 68 private final Handler mHandler; 69 private Listener mListener; 70 71 private PipControlButtonView mFullButtonView; 72 private PipControlButtonView mCloseButtonView; 73 private PipControlButtonView mPlayPauseButtonView; 74 private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>(); 75 private List<RemoteAction> mCustomActions = new ArrayList<>(); 76 77 private PipControlButtonView mFocusedChild; 78 79 private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { 80 @Override 81 public void onPlaybackStateChanged(PlaybackState state) { 82 updateUserActions(); 83 } 84 }; 85 86 private final PipManager.MediaListener mPipMediaListener = new PipManager.MediaListener() { 87 @Override 88 public void onMediaControllerChanged() { 89 updateMediaController(); 90 } 91 }; 92 93 private final OnFocusChangeListener mFocusChangeListener = new OnFocusChangeListener() { 94 @Override 95 public void onFocusChange(View view, boolean hasFocus) { 96 if (hasFocus) { 97 mFocusedChild = (PipControlButtonView) view; 98 } else if (mFocusedChild == view) { 99 mFocusedChild = null; 100 } 101 } 102 }; 103 PipControlsView(Context context)104 public PipControlsView(Context context) { 105 this(context, null, 0, 0); 106 } 107 PipControlsView(Context context, AttributeSet attrs)108 public PipControlsView(Context context, AttributeSet attrs) { 109 this(context, attrs, 0, 0); 110 } 111 PipControlsView(Context context, AttributeSet attrs, int defStyleAttr)112 public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) { 113 this(context, attrs, defStyleAttr, 0); 114 } 115 PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)116 public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 117 super(context, attrs, defStyleAttr, defStyleRes); 118 mLayoutInflater = (LayoutInflater) getContext() 119 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 120 mLayoutInflater.inflate(R.layout.tv_pip_controls, this); 121 mHandler = new Handler(); 122 123 setOrientation(LinearLayout.HORIZONTAL); 124 setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); 125 } 126 127 @Override onFinishInflate()128 public void onFinishInflate() { 129 super.onFinishInflate(); 130 131 mFullButtonView = findViewById(R.id.full_button); 132 mFullButtonView.setOnFocusChangeListener(mFocusChangeListener); 133 mFullButtonView.setOnClickListener(new View.OnClickListener() { 134 @Override 135 public void onClick(View v) { 136 mPipManager.movePipToFullscreen(); 137 } 138 }); 139 140 mCloseButtonView = findViewById(R.id.close_button); 141 mCloseButtonView.setOnFocusChangeListener(mFocusChangeListener); 142 mCloseButtonView.setOnClickListener(new View.OnClickListener() { 143 @Override 144 public void onClick(View v) { 145 mPipManager.closePip(); 146 if (mListener != null) { 147 mListener.onClosed(); 148 } 149 } 150 }); 151 152 mPlayPauseButtonView = findViewById(R.id.play_pause_button); 153 mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener); 154 mPlayPauseButtonView.setOnClickListener(new View.OnClickListener() { 155 @Override 156 public void onClick(View v) { 157 if (mMediaController == null || mMediaController.getPlaybackState() == null) { 158 return; 159 } 160 long actions = mMediaController.getPlaybackState().getActions(); 161 int state = mMediaController.getPlaybackState().getState(); 162 if (mPipManager.getPlaybackState() == PipManager.PLAYBACK_STATE_PAUSED) { 163 mMediaController.getTransportControls().play(); 164 } else if (mPipManager.getPlaybackState() == PipManager.PLAYBACK_STATE_PLAYING) { 165 mMediaController.getTransportControls().pause(); 166 } 167 // View will be updated later in {@link mMediaControllerCallback} 168 } 169 }); 170 } 171 172 @Override onAttachedToWindow()173 public void onAttachedToWindow() { 174 super.onAttachedToWindow(); 175 updateMediaController(); 176 mPipManager.addMediaListener(mPipMediaListener); 177 } 178 179 @Override onDetachedFromWindow()180 public void onDetachedFromWindow() { 181 super.onDetachedFromWindow(); 182 mPipManager.removeMediaListener(mPipMediaListener); 183 if (mMediaController != null) { 184 mMediaController.unregisterCallback(mMediaControllerCallback); 185 } 186 } 187 updateMediaController()188 private void updateMediaController() { 189 MediaController newController = mPipManager.getMediaController(); 190 if (mMediaController == newController) { 191 return; 192 } 193 if (mMediaController != null) { 194 mMediaController.unregisterCallback(mMediaControllerCallback); 195 } 196 mMediaController = newController; 197 if (mMediaController != null) { 198 mMediaController.registerCallback(mMediaControllerCallback); 199 } 200 updateUserActions(); 201 } 202 203 /** 204 * Updates the actions for the PIP. If there are no custom actions, then the media session 205 * actions are shown. 206 */ updateUserActions()207 private void updateUserActions() { 208 if (!mCustomActions.isEmpty()) { 209 // Ensure we have as many buttons as actions 210 while (mCustomButtonViews.size() < mCustomActions.size()) { 211 PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate( 212 R.layout.tv_pip_custom_control, this, false); 213 addView(buttonView); 214 mCustomButtonViews.add(buttonView); 215 } 216 217 // Update the visibility of all views 218 for (int i = 0; i < mCustomButtonViews.size(); i++) { 219 mCustomButtonViews.get(i).setVisibility(i < mCustomActions.size() 220 ? View.VISIBLE 221 : View.GONE); 222 } 223 224 // Update the state and visibility of the action buttons, and hide the rest 225 for (int i = 0; i < mCustomActions.size(); i++) { 226 final RemoteAction action = mCustomActions.get(i); 227 PipControlButtonView actionView = mCustomButtonViews.get(i); 228 229 // TODO: Check if the action drawable has changed before we reload it 230 action.getIcon().loadDrawableAsync(getContext(), d -> { 231 d.setTint(Color.WHITE); 232 actionView.setImageDrawable(d); 233 }, mHandler); 234 actionView.setText(action.getContentDescription()); 235 if (action.isEnabled()) { 236 actionView.setOnClickListener(v -> { 237 try { 238 action.getActionIntent().send(); 239 } catch (CanceledException e) { 240 Log.w(TAG, "Failed to send action", e); 241 } 242 }); 243 } 244 actionView.setEnabled(action.isEnabled()); 245 actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); 246 } 247 248 // Hide the media session buttons 249 mPlayPauseButtonView.setVisibility(View.GONE); 250 } else { 251 int state = mPipManager.getPlaybackState(); 252 if (state == PipManager.PLAYBACK_STATE_UNAVAILABLE) { 253 mPlayPauseButtonView.setVisibility(View.GONE); 254 } else { 255 mPlayPauseButtonView.setVisibility(View.VISIBLE); 256 if (state == PipManager.PLAYBACK_STATE_PLAYING) { 257 mPlayPauseButtonView.setImageResource(R.drawable.ic_pause_white); 258 mPlayPauseButtonView.setText(R.string.pip_pause); 259 } else { 260 mPlayPauseButtonView.setImageResource(R.drawable.ic_play_arrow_white); 261 mPlayPauseButtonView.setText(R.string.pip_play); 262 } 263 } 264 265 // Hide all the custom action buttons 266 for (int i = 0; i < mCustomButtonViews.size(); i++) { 267 mCustomButtonViews.get(i).setVisibility(View.GONE); 268 } 269 } 270 } 271 272 /** 273 * Resets to initial state. 274 */ reset()275 public void reset() { 276 mFullButtonView.reset(); 277 mCloseButtonView.reset(); 278 mPlayPauseButtonView.reset(); 279 mFullButtonView.requestFocus(); 280 for (int i = 0; i < mCustomButtonViews.size(); i++) { 281 mCustomButtonViews.get(i).reset(); 282 } 283 } 284 285 /** 286 * Sets the {@link Listener} to listen user actions. 287 */ setListener(Listener listener)288 public void setListener(Listener listener) { 289 mListener = listener; 290 } 291 292 /** 293 * Updates the set of activity-defined actions. 294 */ setActions(List<RemoteAction> actions)295 public void setActions(List<RemoteAction> actions) { 296 mCustomActions.clear(); 297 mCustomActions.addAll(actions); 298 updateUserActions(); 299 } 300 301 /** 302 * Returns the focused control button view to animate focused button. 303 */ getFocusedButton()304 PipControlButtonView getFocusedButton() { 305 return mFocusedChild; 306 } 307 } 308