blob: 8dcf528271452e7b7db6f5bb7ddc3c298d400fd6 [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;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040024import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050025import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.ColorStateList;
28import android.graphics.Bitmap;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040029import android.graphics.ImageDecoder;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050030import android.graphics.drawable.Drawable;
31import android.graphics.drawable.GradientDrawable;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040032import android.graphics.drawable.Icon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050033import android.graphics.drawable.RippleDrawable;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040034import android.media.MediaDescription;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050035import android.media.MediaMetadata;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040036import android.media.ThumbnailUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050037import android.media.session.MediaController;
38import android.media.session.MediaSession;
39import android.media.session.PlaybackState;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040040import android.net.Uri;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040041import android.service.media.MediaBrowserService;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050042import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050043import android.view.LayoutInflater;
44import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040045import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050046import android.view.ViewGroup;
47import android.widget.ImageButton;
48import android.widget.ImageView;
49import android.widget.LinearLayout;
50import android.widget.TextView;
51
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040052import androidx.annotation.Nullable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050053import androidx.core.graphics.drawable.RoundedBitmapDrawable;
54import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
55
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040056import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050057import com.android.settingslib.media.MediaDevice;
58import com.android.settingslib.media.MediaOutputSliceConstants;
59import com.android.settingslib.widget.AdaptiveIcon;
60import com.android.systemui.Dependency;
61import com.android.systemui.R;
62import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040063import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040064import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050065
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040066import java.io.IOException;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050067import java.util.List;
68import java.util.concurrent.Executor;
69
70/**
71 * Base media control panel for System UI
72 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040073public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050074 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040075 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040076 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040077 protected final Executor mBackgroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050078
79 private Context mContext;
80 protected LinearLayout mMediaNotifView;
81 private View mSeamless;
82 private MediaSession.Token mToken;
83 private MediaController mController;
84 private int mForegroundColor;
85 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040086 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040087 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040088 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040089 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050090
91 private final int[] mActionIds;
92
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040093 public static final String MEDIA_PREFERENCES = "media_control_prefs";
94 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
95 private SharedPreferences mSharedPrefs;
96 private boolean mCheckedForResumption = false;
97
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050098 // Button IDs used in notifications
99 protected static final int[] NOTIF_ACTION_IDS = {
100 com.android.internal.R.id.action0,
101 com.android.internal.R.id.action1,
102 com.android.internal.R.id.action2,
103 com.android.internal.R.id.action3,
104 com.android.internal.R.id.action4
105 };
106
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400107 // URI fields to try loading album art from
108 private static final String[] ART_URIS = {
109 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
110 MediaMetadata.METADATA_KEY_ART_URI,
111 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
112 };
113
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400114 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500115 @Override
116 public void onSessionDestroyed() {
117 Log.d(TAG, "session destroyed");
118 mController.unregisterCallback(mSessionCallback);
119 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400120 makeInactive();
121 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400122 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400123 public void onPlaybackStateChanged(PlaybackState state) {
124 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
125 // When the playback state is NONE or CONNECTING, transition the player to the
126 // resumption state. State CONNECTING needs to be considered for Cast sessions. Ending
127 // a cast session in YT results in the CONNECTING state, which makes sense if you
128 // thinking of the session as waiting to connect to another cast device.
129 if (s == PlaybackState.STATE_NONE || s == PlaybackState.STATE_CONNECTING) {
130 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400131 clearControls();
132 makeInactive();
133 }
134 }
135 };
136
137 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
138 @Override
139 public void onViewAttachedToWindow(View unused) {
140 makeActive();
141 }
142 @Override
143 public void onViewDetachedFromWindow(View unused) {
144 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500145 }
146 };
147
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400148 private final LocalMediaManager.DeviceCallback mDeviceCallback =
149 new LocalMediaManager.DeviceCallback() {
150 @Override
151 public void onDeviceListUpdate(List<MediaDevice> devices) {
152 if (mLocalMediaManager == null) {
153 return;
154 }
155 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
156 // Check because this can be called several times while changing devices
157 if (mDevice == null || !mDevice.equals(currentDevice)) {
158 mDevice = currentDevice;
159 updateDevice(mDevice);
160 }
161 }
162
163 @Override
164 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
165 if (mDevice == null || !mDevice.equals(device)) {
166 mDevice = device;
167 updateDevice(mDevice);
168 }
169 }
170 };
171
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500172 /**
173 * Initialize a new control panel
174 * @param context
175 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400176 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500177 * @param layoutId layout resource to use for this control panel
178 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400179 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500180 * @param backgroundExecutor background executor, used for processing artwork
181 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400182 public MediaControlPanel(Context context, ViewGroup parent,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400183 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
184 Executor foregroundExecutor, Executor backgroundExecutor) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500185 mContext = context;
186 LayoutInflater inflater = LayoutInflater.from(mContext);
187 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400188 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
189 // mStateListener shouldn't need to be unregistered since this object shares the same
190 // lifecycle with the inflated view. It would be better, however, if this controller used an
191 // attach/detach of views instead of inflating them in the constructor, which would allow
192 // mStateListener to be unregistered in detach.
193 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400194 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500195 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400196 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500197 mBackgroundExecutor = backgroundExecutor;
198 }
199
200 /**
201 * Get the view used to display media controls
202 * @return the view
203 */
204 public View getView() {
205 return mMediaNotifView;
206 }
207
208 /**
209 * Get the context
210 * @return context
211 */
212 public Context getContext() {
213 return mContext;
214 }
215
216 /**
217 * Update the media panel view for the given media session
218 * @param token
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400219 * @param iconDrawable
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400220 * @param largeIcon
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500221 * @param iconColor
222 * @param bgColor
223 * @param contentIntent
224 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400225 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500226 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400227 public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
228 int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
229 String key) {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400230 // Ensure that component names are updated if token has changed
231 if (mToken == null || !mToken.equals(token)) {
232 mToken = token;
233 mServiceComponent = null;
234 mCheckedForResumption = false;
235 }
236
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500237 mForegroundColor = iconColor;
238 mBackgroundColor = bgColor;
239 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400240 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500241
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400242 // Try to find a browser service component for this app
243 // TODO also check for a media button receiver intended for restarting (b/154127084)
244 // Only check if we haven't tried yet or the session token changed
245 String pkgName = mController.getPackageName();
246 if (mServiceComponent == null && !mCheckedForResumption) {
247 Log.d(TAG, "Checking for service component");
248 PackageManager pm = mContext.getPackageManager();
249 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
250 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
251 if (resumeInfo != null) {
252 for (ResolveInfo inf : resumeInfo) {
253 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
254 mBackgroundExecutor.execute(() ->
255 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
256 break;
257 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500258 }
259 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400260 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500261 }
262
263 mController.registerCallback(mSessionCallback);
264
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500265 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
266
267 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400268 if (contentIntent != null) {
269 mMediaNotifView.setOnClickListener(v -> {
270 try {
271 contentIntent.send();
272 // Also close shade
273 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
274 } catch (PendingIntent.CanceledException e) {
275 Log.e(TAG, "Pending intent was canceled", e);
276 }
277 });
278 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500279
280 // App icon
281 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500282 iconDrawable.setTint(mForegroundColor);
283 appIcon.setImageDrawable(iconDrawable);
284
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500285 // Transfer chip
286 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400287 if (mSeamless != null && mLocalMediaManager != null) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500288 mSeamless.setVisibility(View.VISIBLE);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400289 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500290 ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
291 mSeamless.setOnClickListener(v -> {
292 final Intent intent = new Intent()
293 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
294 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
295 mController.getPackageName())
296 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
297 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
298 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
299 });
300 }
301
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400302 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400303
304 // App title (not in mini player)
305 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
306 if (appName != null) {
307 appName.setText(appNameString);
308 appName.setTextColor(mForegroundColor);
309 }
310
311 MediaMetadata mediaMetadata = mController.getMetadata();
312 if (mediaMetadata == null) {
313 Log.e(TAG, "Media metadata was null");
314 return;
315 }
316
317 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
318 if (albumView != null) {
319 // Resize art in a background thread
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400320 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400321 }
322
323 // Song name
324 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
325 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
326 titleText.setText(songName);
327 titleText.setTextColor(mForegroundColor);
328
329 // Artist name (not in mini player)
330 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
331 if (artistText != null) {
332 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
333 artistText.setText(artistName);
334 artistText.setTextColor(mForegroundColor);
335 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500336 }
337
338 /**
339 * Return the token for the current media session
340 * @return the token
341 */
342 public MediaSession.Token getMediaSessionToken() {
343 return mToken;
344 }
345
346 /**
347 * Get the current media controller
348 * @return the controller
349 */
350 public MediaController getController() {
351 return mController;
352 }
353
354 /**
355 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400356 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500357 */
358 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400359 if (mController == null) {
360 return null;
361 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500362 return mController.getPackageName();
363 }
364
365 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400366 * Return the original notification's key
367 * @return The notification key
368 */
369 public String getKey() {
370 return mKey;
371 }
372
373 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500374 * Check whether this player has an attached media session.
375 * @return whether there is a controller with a current media session.
376 */
377 public boolean hasMediaSession() {
378 return mController != null && mController.getPlaybackState() != null;
379 }
380
381 /**
382 * Check whether the media controlled by this player is currently playing
383 * @return whether it is playing, or false if no controller information
384 */
385 public boolean isPlaying() {
386 return isPlaying(mController);
387 }
388
389 /**
390 * Check whether the given controller is currently playing
391 * @param controller media controller to check
392 * @return whether it is playing, or false if no controller information
393 */
394 protected boolean isPlaying(MediaController controller) {
395 if (controller == null) {
396 return false;
397 }
398
399 PlaybackState state = controller.getPlaybackState();
400 if (state == null) {
401 return false;
402 }
403
404 return (state.getState() == PlaybackState.STATE_PLAYING);
405 }
406
407 /**
408 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400409 * @param description media description
410 * @param albumView view to hold the album art
411 */
412 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400413 Bitmap albumArt = null;
414
415 // First try loading from URI
416 albumArt = loadBitmapFromUri(description.getIconUri());
417
418 // Then check bitmap
419 if (albumArt == null) {
420 albumArt = description.getIconBitmap();
421 }
422
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400423 processAlbumArtInternal(albumArt, albumView);
424 }
425
426 /**
427 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500428 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400429 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500430 * @param albumView view to hold the album art
431 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400432 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
433 Bitmap albumArt = null;
434
435 // First look in URI fields
436 for (String field : ART_URIS) {
437 String uriString = metadata.getString(field);
438 if (uriString != null) {
439 albumArt = loadBitmapFromUri(Uri.parse(uriString));
440 if (albumArt != null) {
441 Log.d(TAG, "loaded art from " + field);
442 break;
443 }
444 }
445 }
446
447 // Then check bitmap field
448 if (albumArt == null) {
449 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
450 }
451
452 // Finally try the notification's largeIcon
453 if (albumArt == null && largeIcon != null) {
454 albumArt = largeIcon.getBitmap();
455 }
456
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400457 processAlbumArtInternal(albumArt, albumView);
458 }
459
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400460 /**
461 * Load a bitmap from a URI
462 * @param uri
463 * @return bitmap, or null if couldn't be loaded
464 */
465 private Bitmap loadBitmapFromUri(Uri uri) {
466 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
467 try {
468 return ImageDecoder.decodeBitmap(source);
469 } catch (IOException e) {
470 e.printStackTrace();
471 return null;
472 }
473 }
474
475 /**
476 * Resize and crop the image if provided and update the control view
477 * @param albumArt Bitmap of art to display, or null to hide view
478 * @param albumView View that will hold the art
479 */
480 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
481 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500482 RoundedBitmapDrawable roundedDrawable = null;
483 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400484 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500485 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
486 int albumSize = (int) mContext.getResources().getDimension(
487 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400488 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500489 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
490 roundedDrawable.setCornerRadius(radius);
491 } else {
492 Log.e(TAG, "No album art available");
493 }
494
495 // Now that it's resized, update the UI
496 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400497 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500498 if (result != null) {
499 albumView.setImageDrawable(result);
500 albumView.setVisibility(View.VISIBLE);
501 } else {
502 albumView.setImageDrawable(null);
503 albumView.setVisibility(View.GONE);
504 }
505 });
506 }
507
508 /**
509 * Update the current device information
510 * @param device device information to display
511 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400512 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500513 if (mSeamless == null) {
514 return;
515 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400516 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500517 updateChipInternal(device);
518 });
519 }
520
521 private void updateChipInternal(MediaDevice device) {
522 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
523
524 // Update the outline color
525 LinearLayout viewLayout = (LinearLayout) mSeamless;
526 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
527 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
528 rect.setStroke(2, mForegroundColor);
529 rect.setColor(mBackgroundColor);
530
531 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
532 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
533 deviceName.setTextColor(fgTintList);
534
535 if (device != null) {
536 Drawable icon = device.getIcon();
537 iconView.setVisibility(View.VISIBLE);
538 iconView.setImageTintList(fgTintList);
539
540 if (icon instanceof AdaptiveIcon) {
541 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
542 aIcon.setBackgroundColor(mBackgroundColor);
543 iconView.setImageDrawable(aIcon);
544 } else {
545 iconView.setImageDrawable(icon);
546 }
547 deviceName.setText(device.getName());
548 } else {
549 // Reset to default
550 iconView.setVisibility(View.GONE);
551 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
552 }
553 }
554
555 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400556 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
557 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500558 */
559 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400560 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400561 if (mServiceComponent == null) {
562 // If we don't have a way to resume, just remove the player altogether
563 Log.d(TAG, "Removing unresumable controls");
564 removePlayer();
565 return;
566 }
567 resetButtons();
568 }
569
570 /**
571 * Hide the media buttons and show only a restart button
572 */
573 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500574 // Hide all the old buttons
575 for (int i = 0; i < mActionIds.length; i++) {
576 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
577 if (thisBtn != null) {
578 thisBtn.setVisibility(View.GONE);
579 }
580 }
581
582 // Add a restart button
583 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
584 btn.setOnClickListener(v -> {
585 Log.d(TAG, "Attempting to restart session");
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400586 QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
587 browser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500588 });
589 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
590 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
591 btn.setVisibility(View.VISIBLE);
592 }
593
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400594 private void makeActive() {
595 Assert.isMainThread();
596 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400597 if (mLocalMediaManager != null) {
598 mLocalMediaManager.registerCallback(mDeviceCallback);
599 mLocalMediaManager.startScan();
600 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400601 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500602 }
603 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400604
605 private void makeInactive() {
606 Assert.isMainThread();
607 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400608 if (mLocalMediaManager != null) {
609 mLocalMediaManager.stopScan();
610 mLocalMediaManager.unregisterCallback(mDeviceCallback);
611 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400612 mIsRegistered = false;
613 }
614 }
615
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400616 /**
617 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
618 * component to the list of resumption components
619 */
620 private void tryUpdateResumptionList(ComponentName componentName) {
621 Log.d(TAG, "Testing if we can connect to " + componentName);
622 QSMediaBrowser.testConnection(mContext,
623 new QSMediaBrowser.Callback() {
624 @Override
625 public void onConnected() {
626 Log.d(TAG, "yes we can resume with " + componentName);
627 mServiceComponent = componentName;
628 updateResumptionList(componentName);
629 }
630
631 @Override
632 public void onError() {
633 Log.d(TAG, "Cannot resume with " + componentName);
634 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400635 if (!hasMediaSession()) {
636 // If it's not active and we can't resume, remove
637 removePlayer();
638 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400639 }
640 },
641 componentName);
642 }
643
644 /**
645 * Add the component to the saved list of media browser services, checking for duplicates and
646 * removing older components that exceed the maximum limit
647 * @param componentName
648 */
649 private synchronized void updateResumptionList(ComponentName componentName) {
650 // Add to front of saved list
651 if (mSharedPrefs == null) {
652 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
653 }
654 String componentString = componentName.flattenToString();
655 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
656 if (listString == null) {
657 listString = componentString;
658 } else {
659 String[] components = listString.split(QSMediaBrowser.DELIMITER);
660 StringBuilder updated = new StringBuilder(componentString);
661 int nBrowsers = 1;
662 for (int i = 0; i < components.length
663 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
664 if (componentString.equals(components[i])) {
665 continue;
666 }
667 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
668 nBrowsers++;
669 }
670 listString = updated.toString();
671 }
672 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
673 }
674
675 /**
676 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
677 */
678 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500679}