blob: 8492fef97df54c41e8df55c55754fac012a1718f [file] [log] [blame]
Beth Thibodeau7b6c1782020-03-05 11:43:51 -05001/*
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
17package com.android.systemui.media;
18
19import android.annotation.LayoutRes;
20import android.app.PendingIntent;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.content.res.ColorStateList;
27import android.graphics.Bitmap;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.GradientDrawable;
30import android.graphics.drawable.Icon;
31import android.graphics.drawable.RippleDrawable;
32import android.media.MediaMetadata;
33import android.media.session.MediaController;
34import android.media.session.MediaSession;
35import android.media.session.PlaybackState;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.util.Log;
37import android.view.KeyEvent;
38import android.view.LayoutInflater;
39import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040040import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050041import android.view.ViewGroup;
42import android.widget.ImageButton;
43import android.widget.ImageView;
44import android.widget.LinearLayout;
45import android.widget.TextView;
46
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040047import androidx.annotation.Nullable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050048import androidx.core.graphics.drawable.RoundedBitmapDrawable;
49import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
50
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040051import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050052import com.android.settingslib.media.MediaDevice;
53import com.android.settingslib.media.MediaOutputSliceConstants;
54import com.android.settingslib.widget.AdaptiveIcon;
55import com.android.systemui.Dependency;
56import com.android.systemui.R;
57import com.android.systemui.plugins.ActivityStarter;
58import com.android.systemui.statusbar.NotificationMediaManager;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040059import com.android.systemui.statusbar.NotificationMediaManager.MediaListener;
60import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050061
62import java.util.List;
63import java.util.concurrent.Executor;
64
65/**
66 * Base media control panel for System UI
67 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040068public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050069 private static final String TAG = "MediaControlPanel";
70 private final NotificationMediaManager mMediaManager;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040071 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040072 private final Executor mForegroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050073 private final Executor mBackgroundExecutor;
74
75 private Context mContext;
76 protected LinearLayout mMediaNotifView;
77 private View mSeamless;
78 private MediaSession.Token mToken;
79 private MediaController mController;
80 private int mForegroundColor;
81 private int mBackgroundColor;
82 protected ComponentName mRecvComponent;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040083 private MediaDevice mDevice;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040084 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040085 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050086
87 private final int[] mActionIds;
88
89 // Button IDs used in notifications
90 protected static final int[] NOTIF_ACTION_IDS = {
91 com.android.internal.R.id.action0,
92 com.android.internal.R.id.action1,
93 com.android.internal.R.id.action2,
94 com.android.internal.R.id.action3,
95 com.android.internal.R.id.action4
96 };
97
Robert Snoeberger3cc22222020-03-25 15:36:31 -040098 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050099 @Override
100 public void onSessionDestroyed() {
101 Log.d(TAG, "session destroyed");
102 mController.unregisterCallback(mSessionCallback);
103 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400104 makeInactive();
105 }
106 };
107
108 private final MediaListener mMediaListener = new MediaListener() {
109 @Override
110 public void onMetadataOrStateChanged(MediaMetadata metadata, int state) {
111 if (state == PlaybackState.STATE_NONE) {
112 clearControls();
113 makeInactive();
114 }
115 }
116 };
117
118 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
119 @Override
120 public void onViewAttachedToWindow(View unused) {
121 makeActive();
122 }
123 @Override
124 public void onViewDetachedFromWindow(View unused) {
125 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500126 }
127 };
128
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400129 private final LocalMediaManager.DeviceCallback mDeviceCallback =
130 new LocalMediaManager.DeviceCallback() {
131 @Override
132 public void onDeviceListUpdate(List<MediaDevice> devices) {
133 if (mLocalMediaManager == null) {
134 return;
135 }
136 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
137 // Check because this can be called several times while changing devices
138 if (mDevice == null || !mDevice.equals(currentDevice)) {
139 mDevice = currentDevice;
140 updateDevice(mDevice);
141 }
142 }
143
144 @Override
145 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
146 if (mDevice == null || !mDevice.equals(device)) {
147 mDevice = device;
148 updateDevice(mDevice);
149 }
150 }
151 };
152
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500153 /**
154 * Initialize a new control panel
155 * @param context
156 * @param parent
157 * @param manager
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400158 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500159 * @param layoutId layout resource to use for this control panel
160 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400161 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500162 * @param backgroundExecutor background executor, used for processing artwork
163 */
164 public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400165 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
166 Executor foregroundExecutor, Executor backgroundExecutor) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500167 mContext = context;
168 LayoutInflater inflater = LayoutInflater.from(mContext);
169 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400170 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
171 // mStateListener shouldn't need to be unregistered since this object shares the same
172 // lifecycle with the inflated view. It would be better, however, if this controller used an
173 // attach/detach of views instead of inflating them in the constructor, which would allow
174 // mStateListener to be unregistered in detach.
175 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500176 mMediaManager = manager;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400177 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500178 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400179 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500180 mBackgroundExecutor = backgroundExecutor;
181 }
182
183 /**
184 * Get the view used to display media controls
185 * @return the view
186 */
187 public View getView() {
188 return mMediaNotifView;
189 }
190
191 /**
192 * Get the context
193 * @return context
194 */
195 public Context getContext() {
196 return mContext;
197 }
198
199 /**
200 * Update the media panel view for the given media session
201 * @param token
202 * @param icon
203 * @param iconColor
204 * @param bgColor
205 * @param contentIntent
206 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400207 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500208 */
209 public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400210 int bgColor, PendingIntent contentIntent, String appNameString, String key) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500211 mToken = token;
212 mForegroundColor = iconColor;
213 mBackgroundColor = bgColor;
214 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400215 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500216
217 MediaMetadata mediaMetadata = mController.getMetadata();
218
219 // Try to find a receiver for the media button that matches this app
220 PackageManager pm = mContext.getPackageManager();
221 Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
222 List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
223 if (info != null) {
224 for (ResolveInfo inf : info) {
225 if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
226 mRecvComponent = inf.getComponentInfo().getComponentName();
227 }
228 }
229 }
230
231 mController.registerCallback(mSessionCallback);
232
233 if (mediaMetadata == null) {
234 Log.e(TAG, "Media metadata was null");
235 return;
236 }
237
238 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
239 if (albumView != null) {
240 // Resize art in a background thread
241 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
242 }
243 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
244
245 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400246 if (contentIntent != null) {
247 mMediaNotifView.setOnClickListener(v -> {
248 try {
249 contentIntent.send();
250 // Also close shade
251 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
252 } catch (PendingIntent.CanceledException e) {
253 Log.e(TAG, "Pending intent was canceled", e);
254 }
255 });
256 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500257
258 // App icon
259 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
260 Drawable iconDrawable = icon.loadDrawable(mContext);
261 iconDrawable.setTint(mForegroundColor);
262 appIcon.setImageDrawable(iconDrawable);
263
264 // Song name
265 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
266 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
267 titleText.setText(songName);
268 titleText.setTextColor(mForegroundColor);
269
270 // Not in mini player:
271 // App title
272 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
273 if (appName != null) {
274 appName.setText(appNameString);
275 appName.setTextColor(mForegroundColor);
276 }
277
278 // Artist name
279 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
280 if (artistText != null) {
281 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
282 artistText.setText(artistName);
283 artistText.setTextColor(mForegroundColor);
284 }
285
286 // Transfer chip
287 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400288 if (mSeamless != null && mLocalMediaManager != null) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500289 mSeamless.setVisibility(View.VISIBLE);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400290 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500291 ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
292 mSeamless.setOnClickListener(v -> {
293 final Intent intent = new Intent()
294 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
295 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
296 mController.getPackageName())
297 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
298 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
299 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
300 });
301 }
302
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400303 makeActive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500304 }
305
306 /**
307 * Return the token for the current media session
308 * @return the token
309 */
310 public MediaSession.Token getMediaSessionToken() {
311 return mToken;
312 }
313
314 /**
315 * Get the current media controller
316 * @return the controller
317 */
318 public MediaController getController() {
319 return mController;
320 }
321
322 /**
323 * Get the name of the package associated with the current media controller
324 * @return the package name
325 */
326 public String getMediaPlayerPackage() {
327 return mController.getPackageName();
328 }
329
330 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400331 * Return the original notification's key
332 * @return The notification key
333 */
334 public String getKey() {
335 return mKey;
336 }
337
338 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500339 * Check whether this player has an attached media session.
340 * @return whether there is a controller with a current media session.
341 */
342 public boolean hasMediaSession() {
343 return mController != null && mController.getPlaybackState() != null;
344 }
345
346 /**
347 * Check whether the media controlled by this player is currently playing
348 * @return whether it is playing, or false if no controller information
349 */
350 public boolean isPlaying() {
351 return isPlaying(mController);
352 }
353
354 /**
355 * Check whether the given controller is currently playing
356 * @param controller media controller to check
357 * @return whether it is playing, or false if no controller information
358 */
359 protected boolean isPlaying(MediaController controller) {
360 if (controller == null) {
361 return false;
362 }
363
364 PlaybackState state = controller.getPlaybackState();
365 if (state == null) {
366 return false;
367 }
368
369 return (state.getState() == PlaybackState.STATE_PLAYING);
370 }
371
372 /**
373 * Process album art for layout
374 * @param metadata media metadata
375 * @param albumView view to hold the album art
376 */
377 private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
378 Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
379 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
380 RoundedBitmapDrawable roundedDrawable = null;
381 if (albumArt != null) {
382 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
383 int albumSize = (int) mContext.getResources().getDimension(
384 R.dimen.qs_media_album_size);
385 Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
386 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
387 roundedDrawable.setCornerRadius(radius);
388 } else {
389 Log.e(TAG, "No album art available");
390 }
391
392 // Now that it's resized, update the UI
393 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400394 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500395 if (result != null) {
396 albumView.setImageDrawable(result);
397 albumView.setVisibility(View.VISIBLE);
398 } else {
399 albumView.setImageDrawable(null);
400 albumView.setVisibility(View.GONE);
401 }
402 });
403 }
404
405 /**
406 * Update the current device information
407 * @param device device information to display
408 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400409 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500410 if (mSeamless == null) {
411 return;
412 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400413 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500414 updateChipInternal(device);
415 });
416 }
417
418 private void updateChipInternal(MediaDevice device) {
419 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
420
421 // Update the outline color
422 LinearLayout viewLayout = (LinearLayout) mSeamless;
423 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
424 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
425 rect.setStroke(2, mForegroundColor);
426 rect.setColor(mBackgroundColor);
427
428 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
429 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
430 deviceName.setTextColor(fgTintList);
431
432 if (device != null) {
433 Drawable icon = device.getIcon();
434 iconView.setVisibility(View.VISIBLE);
435 iconView.setImageTintList(fgTintList);
436
437 if (icon instanceof AdaptiveIcon) {
438 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
439 aIcon.setBackgroundColor(mBackgroundColor);
440 iconView.setImageDrawable(aIcon);
441 } else {
442 iconView.setImageDrawable(icon);
443 }
444 deviceName.setText(device.getName());
445 } else {
446 // Reset to default
447 iconView.setVisibility(View.GONE);
448 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
449 }
450 }
451
452 /**
453 * Put controls into a resumption state
454 */
455 public void clearControls() {
456 // Hide all the old buttons
457 for (int i = 0; i < mActionIds.length; i++) {
458 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
459 if (thisBtn != null) {
460 thisBtn.setVisibility(View.GONE);
461 }
462 }
463
464 // Add a restart button
465 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
466 btn.setOnClickListener(v -> {
467 Log.d(TAG, "Attempting to restart session");
468 // Send a media button event to previously found receiver
469 if (mRecvComponent != null) {
470 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
471 intent.setComponent(mRecvComponent);
472 int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
473 intent.putExtra(
474 Intent.EXTRA_KEY_EVENT,
475 new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
476 mContext.sendBroadcast(intent);
477 } else {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500478 // If we don't have a receiver, try relaunching the activity instead
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400479 if (mController.getSessionActivity() != null) {
480 try {
481 mController.getSessionActivity().send();
482 } catch (PendingIntent.CanceledException e) {
483 Log.e(TAG, "Pending intent was canceled", e);
484 }
485 } else {
486 Log.e(TAG, "No receiver or activity to restart");
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500487 }
488 }
489 });
490 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
491 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
492 btn.setVisibility(View.VISIBLE);
493 }
494
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400495 private void makeActive() {
496 Assert.isMainThread();
497 if (!mIsRegistered) {
498 mMediaManager.addCallback(mMediaListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400499 if (mLocalMediaManager != null) {
500 mLocalMediaManager.registerCallback(mDeviceCallback);
501 mLocalMediaManager.startScan();
502 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400503 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500504 }
505 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400506
507 private void makeInactive() {
508 Assert.isMainThread();
509 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400510 if (mLocalMediaManager != null) {
511 mLocalMediaManager.stopScan();
512 mLocalMediaManager.unregisterCallback(mDeviceCallback);
513 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400514 mMediaManager.removeCallback(mMediaListener);
515 mIsRegistered = false;
516 }
517 }
518
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500519}