blob: b12d02d26b9be1a0f0b2f552b0a8a83c4ebe7fc4 [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;
Beth Thibodeaudba74bc2020-04-27 14:17:08 -040022import android.content.ContentResolver;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050023import android.content.Context;
24import android.content.Intent;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040025import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050026import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.content.res.ColorStateList;
29import android.graphics.Bitmap;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040030import android.graphics.ImageDecoder;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050031import android.graphics.drawable.Drawable;
32import android.graphics.drawable.GradientDrawable;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040033import android.graphics.drawable.Icon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050034import android.graphics.drawable.RippleDrawable;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040035import android.media.MediaDescription;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.media.MediaMetadata;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040037import android.media.ThumbnailUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050038import android.media.session.MediaController;
Robert Snoebergerc981dc92020-04-27 15:00:50 -040039import android.media.session.MediaController.PlaybackInfo;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050040import android.media.session.MediaSession;
41import android.media.session.PlaybackState;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040042import android.net.Uri;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040043import android.service.media.MediaBrowserService;
Beth Thibodeaudba74bc2020-04-27 14:17:08 -040044import android.text.TextUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050045import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050046import android.view.LayoutInflater;
47import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040048import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050049import android.view.ViewGroup;
50import android.widget.ImageButton;
51import android.widget.ImageView;
52import android.widget.LinearLayout;
53import android.widget.TextView;
54
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040055import androidx.annotation.Nullable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050056import androidx.core.graphics.drawable.RoundedBitmapDrawable;
57import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
58
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040059import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050060import com.android.settingslib.media.MediaDevice;
61import com.android.settingslib.media.MediaOutputSliceConstants;
62import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050063import com.android.systemui.R;
64import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040065import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040066import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050067
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040068import java.io.IOException;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050069import java.util.List;
70import java.util.concurrent.Executor;
71
72/**
73 * Base media control panel for System UI
74 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040075public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050076 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040077 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040078 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040079 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040080 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050081
82 private Context mContext;
83 protected LinearLayout mMediaNotifView;
84 private View mSeamless;
85 private MediaSession.Token mToken;
86 private MediaController mController;
87 private int mForegroundColor;
88 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040089 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040090 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040091 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040092 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050093
94 private final int[] mActionIds;
95
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040096 public static final String MEDIA_PREFERENCES = "media_control_prefs";
97 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
98 private SharedPreferences mSharedPrefs;
99 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400100 private boolean mIsRemotePlayback;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400101
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500102 // Button IDs used in notifications
103 protected static final int[] NOTIF_ACTION_IDS = {
104 com.android.internal.R.id.action0,
105 com.android.internal.R.id.action1,
106 com.android.internal.R.id.action2,
107 com.android.internal.R.id.action3,
108 com.android.internal.R.id.action4
109 };
110
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400111 // URI fields to try loading album art from
112 private static final String[] ART_URIS = {
113 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
114 MediaMetadata.METADATA_KEY_ART_URI,
115 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
116 };
117
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400118 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500119 @Override
120 public void onSessionDestroyed() {
121 Log.d(TAG, "session destroyed");
122 mController.unregisterCallback(mSessionCallback);
123 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400124 makeInactive();
125 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400126 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400127 public void onPlaybackStateChanged(PlaybackState state) {
128 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
129 // When the playback state is NONE or CONNECTING, transition the player to the
130 // resumption state. State CONNECTING needs to be considered for Cast sessions. Ending
131 // a cast session in YT results in the CONNECTING state, which makes sense if you
132 // thinking of the session as waiting to connect to another cast device.
133 if (s == PlaybackState.STATE_NONE || s == PlaybackState.STATE_CONNECTING) {
134 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400135 clearControls();
136 makeInactive();
137 }
138 }
139 };
140
141 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
142 @Override
143 public void onViewAttachedToWindow(View unused) {
144 makeActive();
145 }
146 @Override
147 public void onViewDetachedFromWindow(View unused) {
148 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500149 }
150 };
151
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400152 private final LocalMediaManager.DeviceCallback mDeviceCallback =
153 new LocalMediaManager.DeviceCallback() {
154 @Override
155 public void onDeviceListUpdate(List<MediaDevice> devices) {
156 if (mLocalMediaManager == null) {
157 return;
158 }
159 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
160 // Check because this can be called several times while changing devices
161 if (mDevice == null || !mDevice.equals(currentDevice)) {
162 mDevice = currentDevice;
163 updateDevice(mDevice);
164 }
165 }
166
167 @Override
168 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
169 if (mDevice == null || !mDevice.equals(device)) {
170 mDevice = device;
171 updateDevice(mDevice);
172 }
173 }
174 };
175
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500176 /**
177 * Initialize a new control panel
178 * @param context
179 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400180 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500181 * @param layoutId layout resource to use for this control panel
182 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400183 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500184 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400185 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500186 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400187 public MediaControlPanel(Context context, ViewGroup parent,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400188 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
Beth Thibodeaue561c002020-04-23 17:33:00 -0400189 Executor foregroundExecutor, Executor backgroundExecutor,
190 ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500191 mContext = context;
192 LayoutInflater inflater = LayoutInflater.from(mContext);
193 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400194 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
195 // mStateListener shouldn't need to be unregistered since this object shares the same
196 // lifecycle with the inflated view. It would be better, however, if this controller used an
197 // attach/detach of views instead of inflating them in the constructor, which would allow
198 // mStateListener to be unregistered in detach.
199 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400200 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500201 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400202 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500203 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400204 mActivityStarter = activityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500205 }
206
207 /**
208 * Get the view used to display media controls
209 * @return the view
210 */
211 public View getView() {
212 return mMediaNotifView;
213 }
214
215 /**
216 * Get the context
217 * @return context
218 */
219 public Context getContext() {
220 return mContext;
221 }
222
223 /**
224 * Update the media panel view for the given media session
225 * @param token
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400226 * @param iconDrawable
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400227 * @param largeIcon
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500228 * @param iconColor
229 * @param bgColor
230 * @param contentIntent
231 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400232 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500233 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400234 public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
235 int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
236 String key) {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400237 // Ensure that component names are updated if token has changed
238 if (mToken == null || !mToken.equals(token)) {
239 mToken = token;
240 mServiceComponent = null;
241 mCheckedForResumption = false;
242 }
243
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500244 mForegroundColor = iconColor;
245 mBackgroundColor = bgColor;
246 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400247 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500248
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400249 // Try to find a browser service component for this app
250 // TODO also check for a media button receiver intended for restarting (b/154127084)
251 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400252 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400253 if (mServiceComponent == null && !mCheckedForResumption) {
254 Log.d(TAG, "Checking for service component");
255 PackageManager pm = mContext.getPackageManager();
256 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
257 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
258 if (resumeInfo != null) {
259 for (ResolveInfo inf : resumeInfo) {
260 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
261 mBackgroundExecutor.execute(() ->
262 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
263 break;
264 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500265 }
266 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400267 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500268 }
269
270 mController.registerCallback(mSessionCallback);
271
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500272 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
273
274 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400275 if (contentIntent != null) {
276 mMediaNotifView.setOnClickListener(v -> {
Beth Thibodeaue561c002020-04-23 17:33:00 -0400277 mActivityStarter.postStartActivityDismissingKeyguard(contentIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400278 });
279 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500280
281 // App icon
282 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500283 iconDrawable.setTint(mForegroundColor);
284 appIcon.setImageDrawable(iconDrawable);
285
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500286 // Transfer chip
287 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400288 if (mSeamless != null) {
289 if (mLocalMediaManager != null) {
290 mSeamless.setVisibility(View.VISIBLE);
291 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
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 } else {
302 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
303 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500304 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400305 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
306 if (playbackInfo != null) {
307 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
308 } else {
309 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
310 mIsRemotePlayback = false;
311 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500312
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400313 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400314
315 // App title (not in mini player)
316 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
317 if (appName != null) {
318 appName.setText(appNameString);
319 appName.setTextColor(mForegroundColor);
320 }
321
322 MediaMetadata mediaMetadata = mController.getMetadata();
323 if (mediaMetadata == null) {
324 Log.e(TAG, "Media metadata was null");
325 return;
326 }
327
328 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
329 if (albumView != null) {
330 // Resize art in a background thread
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400331 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400332 }
333
334 // Song name
335 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
336 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
337 titleText.setText(songName);
338 titleText.setTextColor(mForegroundColor);
339
340 // Artist name (not in mini player)
341 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
342 if (artistText != null) {
343 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
344 artistText.setText(artistName);
345 artistText.setTextColor(mForegroundColor);
346 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500347 }
348
349 /**
350 * Return the token for the current media session
351 * @return the token
352 */
353 public MediaSession.Token getMediaSessionToken() {
354 return mToken;
355 }
356
357 /**
358 * Get the current media controller
359 * @return the controller
360 */
361 public MediaController getController() {
362 return mController;
363 }
364
365 /**
366 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400367 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500368 */
369 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400370 if (mController == null) {
371 return null;
372 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500373 return mController.getPackageName();
374 }
375
376 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400377 * Return the original notification's key
378 * @return The notification key
379 */
380 public String getKey() {
381 return mKey;
382 }
383
384 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500385 * Check whether this player has an attached media session.
386 * @return whether there is a controller with a current media session.
387 */
388 public boolean hasMediaSession() {
389 return mController != null && mController.getPlaybackState() != null;
390 }
391
392 /**
393 * Check whether the media controlled by this player is currently playing
394 * @return whether it is playing, or false if no controller information
395 */
396 public boolean isPlaying() {
397 return isPlaying(mController);
398 }
399
400 /**
401 * Check whether the given controller is currently playing
402 * @param controller media controller to check
403 * @return whether it is playing, or false if no controller information
404 */
405 protected boolean isPlaying(MediaController controller) {
406 if (controller == null) {
407 return false;
408 }
409
410 PlaybackState state = controller.getPlaybackState();
411 if (state == null) {
412 return false;
413 }
414
415 return (state.getState() == PlaybackState.STATE_PLAYING);
416 }
417
418 /**
419 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400420 * @param description media description
421 * @param albumView view to hold the album art
422 */
423 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400424 Bitmap albumArt = null;
425
426 // First try loading from URI
427 albumArt = loadBitmapFromUri(description.getIconUri());
428
429 // Then check bitmap
430 if (albumArt == null) {
431 albumArt = description.getIconBitmap();
432 }
433
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400434 processAlbumArtInternal(albumArt, albumView);
435 }
436
437 /**
438 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500439 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400440 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500441 * @param albumView view to hold the album art
442 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400443 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
444 Bitmap albumArt = null;
445
446 // First look in URI fields
447 for (String field : ART_URIS) {
448 String uriString = metadata.getString(field);
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400449 if (!TextUtils.isEmpty(uriString)) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400450 albumArt = loadBitmapFromUri(Uri.parse(uriString));
451 if (albumArt != null) {
452 Log.d(TAG, "loaded art from " + field);
453 break;
454 }
455 }
456 }
457
458 // Then check bitmap field
459 if (albumArt == null) {
460 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
461 }
462
463 // Finally try the notification's largeIcon
464 if (albumArt == null && largeIcon != null) {
465 albumArt = largeIcon.getBitmap();
466 }
467
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400468 processAlbumArtInternal(albumArt, albumView);
469 }
470
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400471 /**
472 * Load a bitmap from a URI
473 * @param uri
474 * @return bitmap, or null if couldn't be loaded
475 */
476 private Bitmap loadBitmapFromUri(Uri uri) {
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400477 // ImageDecoder requires a scheme of the following types
478 if (uri.getScheme() == null) {
479 return null;
480 }
481
482 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
483 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
484 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
485 return null;
486 }
487
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400488 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
489 try {
490 return ImageDecoder.decodeBitmap(source);
491 } catch (IOException e) {
492 e.printStackTrace();
493 return null;
494 }
495 }
496
497 /**
498 * Resize and crop the image if provided and update the control view
499 * @param albumArt Bitmap of art to display, or null to hide view
500 * @param albumView View that will hold the art
501 */
502 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
503 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500504 RoundedBitmapDrawable roundedDrawable = null;
505 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400506 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500507 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
508 int albumSize = (int) mContext.getResources().getDimension(
509 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400510 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500511 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
512 roundedDrawable.setCornerRadius(radius);
513 } else {
514 Log.e(TAG, "No album art available");
515 }
516
517 // Now that it's resized, update the UI
518 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400519 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500520 if (result != null) {
521 albumView.setImageDrawable(result);
522 albumView.setVisibility(View.VISIBLE);
523 } else {
524 albumView.setImageDrawable(null);
525 albumView.setVisibility(View.GONE);
526 }
527 });
528 }
529
530 /**
531 * Update the current device information
532 * @param device device information to display
533 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400534 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500535 if (mSeamless == null) {
536 return;
537 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400538 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500539 updateChipInternal(device);
540 });
541 }
542
543 private void updateChipInternal(MediaDevice device) {
544 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
545
546 // Update the outline color
547 LinearLayout viewLayout = (LinearLayout) mSeamless;
548 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
549 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
550 rect.setStroke(2, mForegroundColor);
551 rect.setColor(mBackgroundColor);
552
553 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
554 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
555 deviceName.setTextColor(fgTintList);
556
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400557 if (mIsRemotePlayback) {
558 mSeamless.setEnabled(false);
559 mSeamless.setAlpha(0.38f);
560 iconView.setImageResource(R.drawable.ic_hardware_speaker);
561 iconView.setVisibility(View.VISIBLE);
562 iconView.setImageTintList(fgTintList);
563 deviceName.setText(R.string.media_seamless_remote_device);
564 } else if (device != null) {
565 mSeamless.setEnabled(true);
566 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500567 Drawable icon = device.getIcon();
568 iconView.setVisibility(View.VISIBLE);
569 iconView.setImageTintList(fgTintList);
570
571 if (icon instanceof AdaptiveIcon) {
572 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
573 aIcon.setBackgroundColor(mBackgroundColor);
574 iconView.setImageDrawable(aIcon);
575 } else {
576 iconView.setImageDrawable(icon);
577 }
578 deviceName.setText(device.getName());
579 } else {
580 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400581 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400582 mSeamless.setEnabled(true);
583 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500584 iconView.setVisibility(View.GONE);
585 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
586 }
587 }
588
589 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400590 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
591 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500592 */
593 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400594 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400595 if (mServiceComponent == null) {
596 // If we don't have a way to resume, just remove the player altogether
597 Log.d(TAG, "Removing unresumable controls");
598 removePlayer();
599 return;
600 }
601 resetButtons();
602 }
603
604 /**
605 * Hide the media buttons and show only a restart button
606 */
607 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500608 // Hide all the old buttons
609 for (int i = 0; i < mActionIds.length; i++) {
610 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
611 if (thisBtn != null) {
612 thisBtn.setVisibility(View.GONE);
613 }
614 }
615
616 // Add a restart button
617 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
618 btn.setOnClickListener(v -> {
619 Log.d(TAG, "Attempting to restart session");
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400620 QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
621 browser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500622 });
623 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
624 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
625 btn.setVisibility(View.VISIBLE);
626 }
627
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400628 private void makeActive() {
629 Assert.isMainThread();
630 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400631 if (mLocalMediaManager != null) {
632 mLocalMediaManager.registerCallback(mDeviceCallback);
633 mLocalMediaManager.startScan();
634 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400635 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500636 }
637 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400638
639 private void makeInactive() {
640 Assert.isMainThread();
641 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400642 if (mLocalMediaManager != null) {
643 mLocalMediaManager.stopScan();
644 mLocalMediaManager.unregisterCallback(mDeviceCallback);
645 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400646 mIsRegistered = false;
647 }
648 }
649
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400650 /**
651 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
652 * component to the list of resumption components
653 */
654 private void tryUpdateResumptionList(ComponentName componentName) {
655 Log.d(TAG, "Testing if we can connect to " + componentName);
656 QSMediaBrowser.testConnection(mContext,
657 new QSMediaBrowser.Callback() {
658 @Override
659 public void onConnected() {
660 Log.d(TAG, "yes we can resume with " + componentName);
661 mServiceComponent = componentName;
662 updateResumptionList(componentName);
663 }
664
665 @Override
666 public void onError() {
667 Log.d(TAG, "Cannot resume with " + componentName);
668 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400669 if (!hasMediaSession()) {
670 // If it's not active and we can't resume, remove
671 removePlayer();
672 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400673 }
674 },
675 componentName);
676 }
677
678 /**
679 * Add the component to the saved list of media browser services, checking for duplicates and
680 * removing older components that exceed the maximum limit
681 * @param componentName
682 */
683 private synchronized void updateResumptionList(ComponentName componentName) {
684 // Add to front of saved list
685 if (mSharedPrefs == null) {
686 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
687 }
688 String componentString = componentName.flattenToString();
689 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
690 if (listString == null) {
691 listString = componentString;
692 } else {
693 String[] components = listString.split(QSMediaBrowser.DELIMITER);
694 StringBuilder updated = new StringBuilder(componentString);
695 int nBrowsers = 1;
696 for (int i = 0; i < components.length
697 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
698 if (componentString.equals(components[i])) {
699 continue;
700 }
701 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
702 nBrowsers++;
703 }
704 listString = updated.toString();
705 }
706 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
707 }
708
709 /**
710 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
711 */
712 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500713}