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