blob: 9adfe7da44077d73547910d4522aaaecc261bdea [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;
Selim Cinekd8357922020-04-10 15:06:53 -070056import androidx.constraintlayout.motion.widget.MotionLayout;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050057import androidx.core.graphics.drawable.RoundedBitmapDrawable;
58import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
59
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040060import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050061import com.android.settingslib.media.MediaDevice;
62import com.android.settingslib.media.MediaOutputSliceConstants;
63import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050064import com.android.systemui.R;
65import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040066import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040067import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050068
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040069import java.io.IOException;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050070import java.util.List;
71import java.util.concurrent.Executor;
72
73/**
Selim Cinekd8357922020-04-10 15:06:53 -070074 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050075 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040076public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050077 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040078 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040079 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040080 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040081 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050082
83 private Context mContext;
Selim Cinekd8357922020-04-10 15:06:53 -070084 protected MotionLayout mMediaNotifView;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050085 private View mSeamless;
86 private MediaSession.Token mToken;
87 private MediaController mController;
88 private int mForegroundColor;
89 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040090 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040091 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040092 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040093 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050094
95 private final int[] mActionIds;
96
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040097 public static final String MEDIA_PREFERENCES = "media_control_prefs";
98 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
99 private SharedPreferences mSharedPrefs;
100 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400101 private boolean mIsRemotePlayback;
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400102 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400103
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500104 // Button IDs used in notifications
105 protected static final int[] NOTIF_ACTION_IDS = {
106 com.android.internal.R.id.action0,
107 com.android.internal.R.id.action1,
108 com.android.internal.R.id.action2,
109 com.android.internal.R.id.action3,
110 com.android.internal.R.id.action4
111 };
112
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400113 // URI fields to try loading album art from
114 private static final String[] ART_URIS = {
115 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
116 MediaMetadata.METADATA_KEY_ART_URI,
117 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
118 };
119
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400120 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500121 @Override
122 public void onSessionDestroyed() {
123 Log.d(TAG, "session destroyed");
124 mController.unregisterCallback(mSessionCallback);
125 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400126 makeInactive();
127 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400128 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400129 public void onPlaybackStateChanged(PlaybackState state) {
130 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400131 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400132 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400133 clearControls();
134 makeInactive();
135 }
136 }
137 };
138
139 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
140 @Override
141 public void onViewAttachedToWindow(View unused) {
142 makeActive();
143 }
144 @Override
145 public void onViewDetachedFromWindow(View unused) {
146 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500147 }
148 };
149
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400150 private final LocalMediaManager.DeviceCallback mDeviceCallback =
151 new LocalMediaManager.DeviceCallback() {
152 @Override
153 public void onDeviceListUpdate(List<MediaDevice> devices) {
154 if (mLocalMediaManager == null) {
155 return;
156 }
157 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
158 // Check because this can be called several times while changing devices
159 if (mDevice == null || !mDevice.equals(currentDevice)) {
160 mDevice = currentDevice;
161 updateDevice(mDevice);
162 }
163 }
164
165 @Override
166 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
167 if (mDevice == null || !mDevice.equals(device)) {
168 mDevice = device;
169 updateDevice(mDevice);
170 }
171 }
172 };
173
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500174 /**
175 * Initialize a new control panel
176 * @param context
177 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400178 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500179 * @param layoutId layout resource to use for this control panel
180 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400181 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500182 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400183 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500184 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400185 public MediaControlPanel(Context context, ViewGroup parent,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400186 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
Beth Thibodeaue561c002020-04-23 17:33:00 -0400187 Executor foregroundExecutor, Executor backgroundExecutor,
188 ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500189 mContext = context;
190 LayoutInflater inflater = LayoutInflater.from(mContext);
Selim Cinekd8357922020-04-10 15:06:53 -0700191 mMediaNotifView = (MotionLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400192 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
193 // mStateListener shouldn't need to be unregistered since this object shares the same
194 // lifecycle with the inflated view. It would be better, however, if this controller used an
195 // attach/detach of views instead of inflating them in the constructor, which would allow
196 // mStateListener to be unregistered in detach.
197 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400198 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500199 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400200 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500201 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400202 mActivityStarter = activityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500203 }
204
205 /**
206 * Get the view used to display media controls
207 * @return the view
208 */
209 public View getView() {
210 return mMediaNotifView;
211 }
212
213 /**
214 * Get the context
215 * @return context
216 */
217 public Context getContext() {
218 return mContext;
219 }
220
221 /**
222 * Update the media panel view for the given media session
223 * @param token
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400224 * @param iconDrawable
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400225 * @param largeIcon
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500226 * @param iconColor
227 * @param bgColor
228 * @param contentIntent
229 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400230 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500231 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400232 public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
233 int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
234 String key) {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400235 // Ensure that component names are updated if token has changed
236 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400237 if (mQSMediaBrowser != null) {
238 Log.d(TAG, "Disconnecting old media browser");
239 mQSMediaBrowser.disconnect();
240 mQSMediaBrowser = null;
241 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400242 mToken = token;
243 mServiceComponent = null;
244 mCheckedForResumption = false;
245 }
246
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500247 mForegroundColor = iconColor;
248 mBackgroundColor = bgColor;
249 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400250 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500251
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400252 // Try to find a browser service component for this app
253 // TODO also check for a media button receiver intended for restarting (b/154127084)
254 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400255 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400256 if (mServiceComponent == null && !mCheckedForResumption) {
257 Log.d(TAG, "Checking for service component");
258 PackageManager pm = mContext.getPackageManager();
259 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
260 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
261 if (resumeInfo != null) {
262 for (ResolveInfo inf : resumeInfo) {
263 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
264 mBackgroundExecutor.execute(() ->
265 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
266 break;
267 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500268 }
269 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400270 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500271 }
272
273 mController.registerCallback(mSessionCallback);
274
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500275 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
276
277 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400278 if (contentIntent != null) {
279 mMediaNotifView.setOnClickListener(v -> {
Beth Thibodeaue561c002020-04-23 17:33:00 -0400280 mActivityStarter.postStartActivityDismissingKeyguard(contentIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400281 });
282 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500283
284 // App icon
285 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500286 iconDrawable.setTint(mForegroundColor);
287 appIcon.setImageDrawable(iconDrawable);
288
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500289 // Transfer chip
290 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400291 if (mSeamless != null) {
292 if (mLocalMediaManager != null) {
293 mSeamless.setVisibility(View.VISIBLE);
294 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
295 mSeamless.setOnClickListener(v -> {
296 final Intent intent = new Intent()
297 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
298 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
299 mController.getPackageName())
300 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
301 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
302 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
303 });
304 } else {
305 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
306 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500307 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400308 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
309 if (playbackInfo != null) {
310 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
311 } else {
312 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
313 mIsRemotePlayback = false;
314 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500315
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400316 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400317
318 // App title (not in mini player)
319 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
320 if (appName != null) {
321 appName.setText(appNameString);
322 appName.setTextColor(mForegroundColor);
323 }
324
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400325 // Can be null!
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400326 MediaMetadata mediaMetadata = mController.getMetadata();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400327
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);
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400336 String songName = "";
337 if (mediaMetadata != null) {
338 songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
339 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400340 titleText.setText(songName);
341 titleText.setTextColor(mForegroundColor);
342
343 // Artist name (not in mini player)
344 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
345 if (artistText != null) {
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400346 String artistName = "";
347 if (mediaMetadata != null) {
348 artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
349 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400350 artistText.setText(artistName);
351 artistText.setTextColor(mForegroundColor);
352 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500353 }
354
355 /**
356 * Return the token for the current media session
357 * @return the token
358 */
359 public MediaSession.Token getMediaSessionToken() {
360 return mToken;
361 }
362
363 /**
364 * Get the current media controller
365 * @return the controller
366 */
367 public MediaController getController() {
368 return mController;
369 }
370
371 /**
372 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400373 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500374 */
375 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400376 if (mController == null) {
377 return null;
378 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500379 return mController.getPackageName();
380 }
381
382 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400383 * Return the original notification's key
384 * @return The notification key
385 */
386 public String getKey() {
387 return mKey;
388 }
389
390 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500391 * Check whether this player has an attached media session.
392 * @return whether there is a controller with a current media session.
393 */
394 public boolean hasMediaSession() {
395 return mController != null && mController.getPlaybackState() != null;
396 }
397
398 /**
399 * Check whether the media controlled by this player is currently playing
400 * @return whether it is playing, or false if no controller information
401 */
402 public boolean isPlaying() {
403 return isPlaying(mController);
404 }
405
406 /**
407 * Check whether the given controller is currently playing
408 * @param controller media controller to check
409 * @return whether it is playing, or false if no controller information
410 */
411 protected boolean isPlaying(MediaController controller) {
412 if (controller == null) {
413 return false;
414 }
415
416 PlaybackState state = controller.getPlaybackState();
417 if (state == null) {
418 return false;
419 }
420
421 return (state.getState() == PlaybackState.STATE_PLAYING);
422 }
423
424 /**
425 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400426 * @param description media description
427 * @param albumView view to hold the album art
428 */
429 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400430 Bitmap albumArt = null;
431
432 // First try loading from URI
433 albumArt = loadBitmapFromUri(description.getIconUri());
434
435 // Then check bitmap
436 if (albumArt == null) {
437 albumArt = description.getIconBitmap();
438 }
439
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400440 processAlbumArtInternal(albumArt, albumView);
441 }
442
443 /**
444 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500445 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400446 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500447 * @param albumView view to hold the album art
448 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400449 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
450 Bitmap albumArt = null;
451
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400452 if (metadata != null) {
453 // First look in URI fields
454 for (String field : ART_URIS) {
455 String uriString = metadata.getString(field);
456 if (!TextUtils.isEmpty(uriString)) {
457 albumArt = loadBitmapFromUri(Uri.parse(uriString));
458 if (albumArt != null) {
459 Log.d(TAG, "loaded art from " + field);
460 break;
461 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400462 }
463 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400464
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400465 // Then check bitmap field
466 if (albumArt == null) {
467 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
468 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400469 }
470
471 // Finally try the notification's largeIcon
472 if (albumArt == null && largeIcon != null) {
473 albumArt = largeIcon.getBitmap();
474 }
475
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400476 processAlbumArtInternal(albumArt, albumView);
477 }
478
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400479 /**
480 * Load a bitmap from a URI
481 * @param uri
482 * @return bitmap, or null if couldn't be loaded
483 */
484 private Bitmap loadBitmapFromUri(Uri uri) {
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400485 // ImageDecoder requires a scheme of the following types
486 if (uri.getScheme() == null) {
487 return null;
488 }
489
490 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
491 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
492 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
493 return null;
494 }
495
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400496 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
497 try {
498 return ImageDecoder.decodeBitmap(source);
499 } catch (IOException e) {
500 e.printStackTrace();
501 return null;
502 }
503 }
504
505 /**
506 * Resize and crop the image if provided and update the control view
507 * @param albumArt Bitmap of art to display, or null to hide view
508 * @param albumView View that will hold the art
509 */
510 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
511 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500512 RoundedBitmapDrawable roundedDrawable = null;
513 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400514 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500515 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
516 int albumSize = (int) mContext.getResources().getDimension(
517 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400518 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500519 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
520 roundedDrawable.setCornerRadius(radius);
521 } else {
522 Log.e(TAG, "No album art available");
523 }
524
525 // Now that it's resized, update the UI
526 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400527 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500528 if (result != null) {
529 albumView.setImageDrawable(result);
530 albumView.setVisibility(View.VISIBLE);
531 } else {
532 albumView.setImageDrawable(null);
533 albumView.setVisibility(View.GONE);
534 }
535 });
536 }
537
538 /**
539 * Update the current device information
540 * @param device device information to display
541 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400542 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500543 if (mSeamless == null) {
544 return;
545 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400546 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500547 updateChipInternal(device);
548 });
549 }
550
551 private void updateChipInternal(MediaDevice device) {
552 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
553
554 // Update the outline color
555 LinearLayout viewLayout = (LinearLayout) mSeamless;
556 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
557 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
558 rect.setStroke(2, mForegroundColor);
559 rect.setColor(mBackgroundColor);
560
561 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
562 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
563 deviceName.setTextColor(fgTintList);
564
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400565 if (mIsRemotePlayback) {
566 mSeamless.setEnabled(false);
567 mSeamless.setAlpha(0.38f);
568 iconView.setImageResource(R.drawable.ic_hardware_speaker);
569 iconView.setVisibility(View.VISIBLE);
570 iconView.setImageTintList(fgTintList);
571 deviceName.setText(R.string.media_seamless_remote_device);
572 } else if (device != null) {
573 mSeamless.setEnabled(true);
574 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500575 Drawable icon = device.getIcon();
576 iconView.setVisibility(View.VISIBLE);
577 iconView.setImageTintList(fgTintList);
578
579 if (icon instanceof AdaptiveIcon) {
580 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
581 aIcon.setBackgroundColor(mBackgroundColor);
582 iconView.setImageDrawable(aIcon);
583 } else {
584 iconView.setImageDrawable(icon);
585 }
586 deviceName.setText(device.getName());
587 } else {
588 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400589 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400590 mSeamless.setEnabled(true);
591 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500592 iconView.setVisibility(View.GONE);
593 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
594 }
595 }
596
597 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400598 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
599 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500600 */
601 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400602 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400603 if (mServiceComponent == null) {
604 // If we don't have a way to resume, just remove the player altogether
605 Log.d(TAG, "Removing unresumable controls");
606 removePlayer();
607 return;
608 }
609 resetButtons();
610 }
611
612 /**
613 * Hide the media buttons and show only a restart button
614 */
615 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500616 // Hide all the old buttons
617 for (int i = 0; i < mActionIds.length; i++) {
618 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
619 if (thisBtn != null) {
620 thisBtn.setVisibility(View.GONE);
621 }
622 }
623
624 // Add a restart button
625 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
626 btn.setOnClickListener(v -> {
627 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400628 if (mQSMediaBrowser != null) {
629 mQSMediaBrowser.disconnect();
630 }
631 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
632 @Override
633 public void onConnected() {
634 Log.d(TAG, "Successfully restarted");
635 }
636 @Override
637 public void onError() {
638 Log.e(TAG, "Error restarting");
639 mQSMediaBrowser.disconnect();
640 mQSMediaBrowser = null;
641 }
642 }, mServiceComponent);
643 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500644 });
645 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
646 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
647 btn.setVisibility(View.VISIBLE);
648 }
649
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400650 private void makeActive() {
651 Assert.isMainThread();
652 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400653 if (mLocalMediaManager != null) {
654 mLocalMediaManager.registerCallback(mDeviceCallback);
655 mLocalMediaManager.startScan();
656 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400657 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500658 }
659 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400660
661 private void makeInactive() {
662 Assert.isMainThread();
663 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400664 if (mLocalMediaManager != null) {
665 mLocalMediaManager.stopScan();
666 mLocalMediaManager.unregisterCallback(mDeviceCallback);
667 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400668 mIsRegistered = false;
669 }
670 }
671
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400672 /**
673 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
674 * component to the list of resumption components
675 */
676 private void tryUpdateResumptionList(ComponentName componentName) {
677 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400678 if (mQSMediaBrowser != null) {
679 mQSMediaBrowser.disconnect();
680 }
681 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400682 new QSMediaBrowser.Callback() {
683 @Override
684 public void onConnected() {
685 Log.d(TAG, "yes we can resume with " + componentName);
686 mServiceComponent = componentName;
687 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400688 mQSMediaBrowser.disconnect();
689 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400690 }
691
692 @Override
693 public void onError() {
694 Log.d(TAG, "Cannot resume with " + componentName);
695 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400696 if (!hasMediaSession()) {
697 // If it's not active and we can't resume, remove
698 removePlayer();
699 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400700 mQSMediaBrowser.disconnect();
701 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400702 }
703 },
704 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400705 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400706 }
707
708 /**
709 * Add the component to the saved list of media browser services, checking for duplicates and
710 * removing older components that exceed the maximum limit
711 * @param componentName
712 */
713 private synchronized void updateResumptionList(ComponentName componentName) {
714 // Add to front of saved list
715 if (mSharedPrefs == null) {
716 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
717 }
718 String componentString = componentName.flattenToString();
719 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
720 if (listString == null) {
721 listString = componentString;
722 } else {
723 String[] components = listString.split(QSMediaBrowser.DELIMITER);
724 StringBuilder updated = new StringBuilder(componentString);
725 int nBrowsers = 1;
726 for (int i = 0; i < components.length
727 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
728 if (componentString.equals(components[i])) {
729 continue;
730 }
731 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
732 nBrowsers++;
733 }
734 listString = updated.toString();
735 }
736 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
737 }
738
739 /**
740 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
741 */
742 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500743}