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