1 /*
2  * Copyright (C) 2020 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.media;
18 
19 import android.app.PendingIntent;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.res.ColorStateList;
23 import android.graphics.Outline;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.Icon;
27 import android.media.session.MediaController;
28 import android.media.session.MediaSession;
29 import android.media.session.PlaybackState;
30 import android.util.Log;
31 import android.view.View;
32 import android.view.ViewOutlineProvider;
33 import android.widget.ImageButton;
34 import android.widget.ImageView;
35 import android.widget.TextView;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.UiThread;
40 import androidx.constraintlayout.widget.ConstraintSet;
41 
42 import com.android.settingslib.Utils;
43 import com.android.settingslib.media.MediaOutputSliceConstants;
44 import com.android.settingslib.widget.AdaptiveIcon;
45 import com.android.systemui.R;
46 import com.android.systemui.dagger.qualifiers.Background;
47 import com.android.systemui.plugins.ActivityStarter;
48 import com.android.systemui.util.animation.TransitionLayout;
49 
50 import java.util.List;
51 import java.util.concurrent.Executor;
52 
53 import javax.inject.Inject;
54 
55 /**
56  * A view controller used for Media Playback.
57  */
58 public class MediaControlPanel {
59     private static final String TAG = "MediaControlPanel";
60     private static final float DISABLED_ALPHA = 0.38f;
61 
62     // Button IDs for QS controls
63     static final int[] ACTION_IDS = {
64             R.id.action0,
65             R.id.action1,
66             R.id.action2,
67             R.id.action3,
68             R.id.action4
69     };
70 
71     private final SeekBarViewModel mSeekBarViewModel;
72     private SeekBarObserver mSeekBarObserver;
73     protected final Executor mBackgroundExecutor;
74     private final ActivityStarter mActivityStarter;
75 
76     private Context mContext;
77     private PlayerViewHolder mViewHolder;
78     private MediaViewController mMediaViewController;
79     private MediaSession.Token mToken;
80     private MediaController mController;
81     private int mBackgroundColor;
82     private int mAlbumArtSize;
83     private int mAlbumArtRadius;
84     // This will provide the corners for the album art.
85     private final ViewOutlineProvider mViewOutlineProvider;
86 
87     /**
88      * Initialize a new control panel
89      * @param context
90      * @param backgroundExecutor background executor, used for processing artwork
91      * @param activityStarter activity starter
92      */
93     @Inject
MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel)94     public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
95             ActivityStarter activityStarter, MediaViewController mediaViewController,
96             SeekBarViewModel seekBarViewModel) {
97         mContext = context;
98         mBackgroundExecutor = backgroundExecutor;
99         mActivityStarter = activityStarter;
100         mSeekBarViewModel = seekBarViewModel;
101         mMediaViewController = mediaViewController;
102         loadDimens();
103 
104         mViewOutlineProvider = new ViewOutlineProvider() {
105             @Override
106             public void getOutline(View view, Outline outline) {
107                 outline.setRoundRect(0, 0, mAlbumArtSize, mAlbumArtSize, mAlbumArtRadius);
108             }
109         };
110     }
111 
onDestroy()112     public void onDestroy() {
113         if (mSeekBarObserver != null) {
114             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
115         }
116         mSeekBarViewModel.onDestroy();
117         mMediaViewController.onDestroy();
118     }
119 
loadDimens()120     private void loadDimens() {
121         mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
122                 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
123         mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
124     }
125 
126     /**
127      * Get the view holder used to display media controls
128      * @return the view holder
129      */
130     @Nullable
getView()131     public PlayerViewHolder getView() {
132         return mViewHolder;
133     }
134 
135     /**
136      * Get the view controller used to display media controls
137      * @return the media view controller
138      */
139     @NonNull
getMediaViewController()140     public MediaViewController getMediaViewController() {
141         return mMediaViewController;
142     }
143 
144     /**
145      * Sets the listening state of the player.
146      *
147      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
148      * unnecessary work when the QS panel is closed.
149      *
150      * @param listening True when player should be active. Otherwise, false.
151      */
setListening(boolean listening)152     public void setListening(boolean listening) {
153         mSeekBarViewModel.setListening(listening);
154     }
155 
156     /**
157      * Get the context
158      * @return context
159      */
getContext()160     public Context getContext() {
161         return mContext;
162     }
163 
164     /** Attaches the player to the view holder. */
attach(PlayerViewHolder vh)165     public void attach(PlayerViewHolder vh) {
166         mViewHolder = vh;
167         TransitionLayout player = vh.getPlayer();
168 
169         ImageView albumView = vh.getAlbumView();
170         albumView.setOutlineProvider(mViewOutlineProvider);
171         albumView.setClipToOutline(true);
172 
173         mSeekBarObserver = new SeekBarObserver(vh);
174         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
175         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
176         mMediaViewController.attach(player);
177     }
178 
179     /**
180      * Bind this view based on the data given
181      */
bind(@onNull MediaData data)182     public void bind(@NonNull MediaData data) {
183         if (mViewHolder == null) {
184             return;
185         }
186         MediaSession.Token token = data.getToken();
187         mBackgroundColor = data.getBackgroundColor();
188         if (mToken == null || !mToken.equals(token)) {
189             mToken = token;
190         }
191 
192         if (mToken != null) {
193             mController = new MediaController(mContext, mToken);
194         } else {
195             mController = null;
196         }
197 
198         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
199         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
200 
201         mViewHolder.getPlayer().setBackgroundTintList(
202                 ColorStateList.valueOf(mBackgroundColor));
203 
204         // Click action
205         PendingIntent clickIntent = data.getClickIntent();
206         if (clickIntent != null) {
207             mViewHolder.getPlayer().setOnClickListener(v -> {
208                 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
209             });
210         }
211 
212         ImageView albumView = mViewHolder.getAlbumView();
213         boolean hasArtwork = data.getArtwork() != null;
214         if (hasArtwork) {
215             Drawable artwork = scaleDrawable(data.getArtwork());
216             albumView.setImageDrawable(artwork);
217         }
218         setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
219         setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
220 
221         // App icon
222         ImageView appIcon = mViewHolder.getAppIcon();
223         if (data.getAppIcon() != null) {
224             appIcon.setImageDrawable(data.getAppIcon());
225         } else {
226             Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
227             appIcon.setImageDrawable(iconDrawable);
228         }
229 
230         // Song name
231         TextView titleText = mViewHolder.getTitleText();
232         titleText.setText(data.getSong());
233 
234         // App title
235         TextView appName = mViewHolder.getAppName();
236         appName.setText(data.getApp());
237 
238         // Artist name
239         TextView artistText = mViewHolder.getArtistText();
240         artistText.setText(data.getArtist());
241 
242         // Transfer chip
243         mViewHolder.getSeamless().setVisibility(View.VISIBLE);
244         setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
245         setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
246         mViewHolder.getSeamless().setOnClickListener(v -> {
247             final Intent intent = new Intent()
248                     .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
249                     .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
250                             data.getPackageName())
251                     .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
252             mActivityStarter.startActivity(intent, false, true /* dismissShade */,
253                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
254         });
255 
256         ImageView iconView = mViewHolder.getSeamlessIcon();
257         TextView deviceName = mViewHolder.getSeamlessText();
258 
259         final MediaDeviceData device = data.getDevice();
260         final int seamlessId = mViewHolder.getSeamless().getId();
261         final int seamlessFallbackId = mViewHolder.getSeamlessFallback().getId();
262         final boolean showFallback = device != null && !device.getEnabled();
263         final int seamlessFallbackVisibility = showFallback ? View.VISIBLE : View.GONE;
264         mViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility);
265         expandedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
266         collapsedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
267         final int seamlessVisibility = showFallback ? View.GONE : View.VISIBLE;
268         mViewHolder.getSeamless().setVisibility(seamlessVisibility);
269         expandedSet.setVisibility(seamlessId, seamlessVisibility);
270         collapsedSet.setVisibility(seamlessId, seamlessVisibility);
271         final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f;
272         expandedSet.setAlpha(seamlessId, seamlessAlpha);
273         collapsedSet.setAlpha(seamlessId, seamlessAlpha);
274         // Disable clicking on output switcher for resumption controls.
275         mViewHolder.getSeamless().setEnabled(!data.getResumption());
276         if (showFallback) {
277             iconView.setImageDrawable(null);
278             deviceName.setText(null);
279         } else if (device != null) {
280             Drawable icon = device.getIcon();
281             iconView.setVisibility(View.VISIBLE);
282             if (icon instanceof AdaptiveIcon) {
283                 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
284                 aIcon.setBackgroundColor(mBackgroundColor);
285                 iconView.setImageDrawable(aIcon);
286             } else {
287                 iconView.setImageDrawable(icon);
288             }
289             deviceName.setText(device.getName());
290         } else {
291             // Reset to default
292             Log.w(TAG, "device is null. Not binding output chip.");
293             iconView.setVisibility(View.GONE);
294             deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
295         }
296 
297         List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
298         // Media controls
299         int i = 0;
300         List<MediaAction> actionIcons = data.getActions();
301         for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
302             int actionId = ACTION_IDS[i];
303             final ImageButton button = mViewHolder.getAction(actionId);
304             MediaAction mediaAction = actionIcons.get(i);
305             button.setImageDrawable(mediaAction.getDrawable());
306             button.setContentDescription(mediaAction.getContentDescription());
307             Runnable action = mediaAction.getAction();
308 
309             if (action == null) {
310                 button.setEnabled(false);
311             } else {
312                 button.setEnabled(true);
313                 button.setOnClickListener(v -> {
314                     action.run();
315                 });
316             }
317             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
318             setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
319             setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
320         }
321 
322         // Hide any unused buttons
323         for (; i < ACTION_IDS.length; i++) {
324             setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
325             setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
326         }
327 
328         // Seek Bar
329         final MediaController controller = getController();
330         mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
331 
332         // Set up long press menu
333         // TODO: b/156036025 bring back media guts
334 
335         // TODO: We don't need to refresh this state constantly, only if the state actually changed
336         // to something which might impact the measurement
337         mMediaViewController.refreshState();
338     }
339 
340     @UiThread
scaleDrawable(Icon icon)341     private Drawable scaleDrawable(Icon icon) {
342         if (icon == null) {
343             return null;
344         }
345         // Let's scale down the View, such that the content always nicely fills the view.
346         // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
347         // ratios
348         Drawable drawable = icon.loadDrawable(mContext);
349         float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
350         Rect bounds;
351         if (aspectRatio > 1.0f) {
352             bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
353         } else {
354             bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
355         }
356         if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
357             float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
358             float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
359             bounds.offset((int) -offsetX,(int) -offsetY);
360         }
361         drawable.setBounds(bounds);
362         return drawable;
363     }
364 
365     /**
366      * Get the current media controller
367      * @return the controller
368      */
getController()369     public MediaController getController() {
370         return mController;
371     }
372 
373     /**
374      * Check whether the media controlled by this player is currently playing
375      * @return whether it is playing, or false if no controller information
376      */
isPlaying()377     public boolean isPlaying() {
378         return isPlaying(mController);
379     }
380 
381     /**
382      * Check whether the given controller is currently playing
383      * @param controller media controller to check
384      * @return whether it is playing, or false if no controller information
385      */
isPlaying(MediaController controller)386     protected boolean isPlaying(MediaController controller) {
387         if (controller == null) {
388             return false;
389         }
390 
391         PlaybackState state = controller.getPlaybackState();
392         if (state == null) {
393             return false;
394         }
395 
396         return (state.getState() == PlaybackState.STATE_PLAYING);
397     }
398 
setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible)399     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
400         set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
401         set.setAlpha(actionId, visible ? 1.0f : 0.0f);
402     }
403 }
404