blob: 557132bdf08e70d6bee1a49a5c858a802fa5cd0e [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 Thibodeauee42e8c2020-04-30 01:09:29 -0400101 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400102
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500103 // Button IDs used in notifications
104 protected static final int[] NOTIF_ACTION_IDS = {
105 com.android.internal.R.id.action0,
106 com.android.internal.R.id.action1,
107 com.android.internal.R.id.action2,
108 com.android.internal.R.id.action3,
109 com.android.internal.R.id.action4
110 };
111
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400112 // URI fields to try loading album art from
113 private static final String[] ART_URIS = {
114 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
115 MediaMetadata.METADATA_KEY_ART_URI,
116 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
117 };
118
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400119 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500120 @Override
121 public void onSessionDestroyed() {
122 Log.d(TAG, "session destroyed");
123 mController.unregisterCallback(mSessionCallback);
124 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400125 makeInactive();
126 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400127 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400128 public void onPlaybackStateChanged(PlaybackState state) {
129 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400130 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400131 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400132 clearControls();
133 makeInactive();
134 }
135 }
136 };
137
138 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
139 @Override
140 public void onViewAttachedToWindow(View unused) {
141 makeActive();
142 }
143 @Override
144 public void onViewDetachedFromWindow(View unused) {
145 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500146 }
147 };
148
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400149 private final LocalMediaManager.DeviceCallback mDeviceCallback =
150 new LocalMediaManager.DeviceCallback() {
151 @Override
152 public void onDeviceListUpdate(List<MediaDevice> devices) {
153 if (mLocalMediaManager == null) {
154 return;
155 }
156 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
157 // Check because this can be called several times while changing devices
158 if (mDevice == null || !mDevice.equals(currentDevice)) {
159 mDevice = currentDevice;
160 updateDevice(mDevice);
161 }
162 }
163
164 @Override
165 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
166 if (mDevice == null || !mDevice.equals(device)) {
167 mDevice = device;
168 updateDevice(mDevice);
169 }
170 }
171 };
172
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500173 /**
174 * Initialize a new control panel
175 * @param context
176 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400177 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500178 * @param layoutId layout resource to use for this control panel
179 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400180 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500181 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400182 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500183 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400184 public MediaControlPanel(Context context, ViewGroup parent,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400185 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
Beth Thibodeaue561c002020-04-23 17:33:00 -0400186 Executor foregroundExecutor, Executor backgroundExecutor,
187 ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500188 mContext = context;
189 LayoutInflater inflater = LayoutInflater.from(mContext);
190 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400191 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
192 // mStateListener shouldn't need to be unregistered since this object shares the same
193 // lifecycle with the inflated view. It would be better, however, if this controller used an
194 // attach/detach of views instead of inflating them in the constructor, which would allow
195 // mStateListener to be unregistered in detach.
196 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400197 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500198 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400199 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500200 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400201 mActivityStarter = activityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500202 }
203
204 /**
205 * Get the view used to display media controls
206 * @return the view
207 */
208 public View getView() {
209 return mMediaNotifView;
210 }
211
212 /**
213 * Get the context
214 * @return context
215 */
216 public Context getContext() {
217 return mContext;
218 }
219
220 /**
221 * Update the media panel view for the given media session
222 * @param token
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400223 * @param iconDrawable
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400224 * @param largeIcon
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500225 * @param iconColor
226 * @param bgColor
227 * @param contentIntent
228 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400229 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500230 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400231 public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
232 int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
233 String key) {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400234 // Ensure that component names are updated if token has changed
235 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400236 if (mQSMediaBrowser != null) {
237 Log.d(TAG, "Disconnecting old media browser");
238 mQSMediaBrowser.disconnect();
239 mQSMediaBrowser = null;
240 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400241 mToken = token;
242 mServiceComponent = null;
243 mCheckedForResumption = false;
244 }
245
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500246 mForegroundColor = iconColor;
247 mBackgroundColor = bgColor;
248 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400249 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500250
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400251 // Try to find a browser service component for this app
252 // TODO also check for a media button receiver intended for restarting (b/154127084)
253 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400254 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400255 if (mServiceComponent == null && !mCheckedForResumption) {
256 Log.d(TAG, "Checking for service component");
257 PackageManager pm = mContext.getPackageManager();
258 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
259 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
260 if (resumeInfo != null) {
261 for (ResolveInfo inf : resumeInfo) {
262 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
263 mBackgroundExecutor.execute(() ->
264 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
265 break;
266 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500267 }
268 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400269 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500270 }
271
272 mController.registerCallback(mSessionCallback);
273
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500274 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
275
276 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400277 if (contentIntent != null) {
278 mMediaNotifView.setOnClickListener(v -> {
Beth Thibodeaue561c002020-04-23 17:33:00 -0400279 mActivityStarter.postStartActivityDismissingKeyguard(contentIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400280 });
281 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500282
283 // App icon
284 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500285 iconDrawable.setTint(mForegroundColor);
286 appIcon.setImageDrawable(iconDrawable);
287
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500288 // Transfer chip
289 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400290 if (mSeamless != null) {
291 if (mLocalMediaManager != null) {
292 mSeamless.setVisibility(View.VISIBLE);
293 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
294 mSeamless.setOnClickListener(v -> {
295 final Intent intent = new Intent()
296 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
297 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
298 mController.getPackageName())
299 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
300 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
301 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
302 });
303 } else {
304 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
305 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500306 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400307 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
308 if (playbackInfo != null) {
309 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
310 } else {
311 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
312 mIsRemotePlayback = false;
313 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500314
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400315 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400316
317 // App title (not in mini player)
318 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
319 if (appName != null) {
320 appName.setText(appNameString);
321 appName.setTextColor(mForegroundColor);
322 }
323
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400324 // Can be null!
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400325 MediaMetadata mediaMetadata = mController.getMetadata();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400326
327 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
328 if (albumView != null) {
329 // Resize art in a background thread
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400330 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400331 }
332
333 // Song name
334 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400335 String songName = "";
336 if (mediaMetadata != null) {
337 songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
338 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400339 titleText.setText(songName);
340 titleText.setTextColor(mForegroundColor);
341
342 // Artist name (not in mini player)
343 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
344 if (artistText != null) {
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400345 String artistName = "";
346 if (mediaMetadata != null) {
347 artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
348 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400349 artistText.setText(artistName);
350 artistText.setTextColor(mForegroundColor);
351 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500352 }
353
354 /**
355 * Return the token for the current media session
356 * @return the token
357 */
358 public MediaSession.Token getMediaSessionToken() {
359 return mToken;
360 }
361
362 /**
363 * Get the current media controller
364 * @return the controller
365 */
366 public MediaController getController() {
367 return mController;
368 }
369
370 /**
371 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400372 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500373 */
374 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400375 if (mController == null) {
376 return null;
377 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500378 return mController.getPackageName();
379 }
380
381 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400382 * Return the original notification's key
383 * @return The notification key
384 */
385 public String getKey() {
386 return mKey;
387 }
388
389 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500390 * Check whether this player has an attached media session.
391 * @return whether there is a controller with a current media session.
392 */
393 public boolean hasMediaSession() {
394 return mController != null && mController.getPlaybackState() != null;
395 }
396
397 /**
398 * Check whether the media controlled by this player is currently playing
399 * @return whether it is playing, or false if no controller information
400 */
401 public boolean isPlaying() {
402 return isPlaying(mController);
403 }
404
405 /**
406 * Check whether the given controller is currently playing
407 * @param controller media controller to check
408 * @return whether it is playing, or false if no controller information
409 */
410 protected boolean isPlaying(MediaController controller) {
411 if (controller == null) {
412 return false;
413 }
414
415 PlaybackState state = controller.getPlaybackState();
416 if (state == null) {
417 return false;
418 }
419
420 return (state.getState() == PlaybackState.STATE_PLAYING);
421 }
422
423 /**
424 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400425 * @param description media description
426 * @param albumView view to hold the album art
427 */
428 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400429 Bitmap albumArt = null;
430
431 // First try loading from URI
432 albumArt = loadBitmapFromUri(description.getIconUri());
433
434 // Then check bitmap
435 if (albumArt == null) {
436 albumArt = description.getIconBitmap();
437 }
438
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400439 processAlbumArtInternal(albumArt, albumView);
440 }
441
442 /**
443 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500444 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400445 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500446 * @param albumView view to hold the album art
447 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400448 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
449 Bitmap albumArt = null;
450
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400451 if (metadata != null) {
452 // First look in URI fields
453 for (String field : ART_URIS) {
454 String uriString = metadata.getString(field);
455 if (!TextUtils.isEmpty(uriString)) {
456 albumArt = loadBitmapFromUri(Uri.parse(uriString));
457 if (albumArt != null) {
458 Log.d(TAG, "loaded art from " + field);
459 break;
460 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400461 }
462 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400463
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400464 // Then check bitmap field
465 if (albumArt == null) {
466 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
467 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400468 }
469
470 // Finally try the notification's largeIcon
471 if (albumArt == null && largeIcon != null) {
472 albumArt = largeIcon.getBitmap();
473 }
474
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400475 processAlbumArtInternal(albumArt, albumView);
476 }
477
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400478 /**
479 * Load a bitmap from a URI
480 * @param uri
481 * @return bitmap, or null if couldn't be loaded
482 */
483 private Bitmap loadBitmapFromUri(Uri uri) {
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400484 // ImageDecoder requires a scheme of the following types
485 if (uri.getScheme() == null) {
486 return null;
487 }
488
489 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
490 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
491 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
492 return null;
493 }
494
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400495 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
496 try {
497 return ImageDecoder.decodeBitmap(source);
498 } catch (IOException e) {
499 e.printStackTrace();
500 return null;
501 }
502 }
503
504 /**
505 * Resize and crop the image if provided and update the control view
506 * @param albumArt Bitmap of art to display, or null to hide view
507 * @param albumView View that will hold the art
508 */
509 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
510 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500511 RoundedBitmapDrawable roundedDrawable = null;
512 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400513 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500514 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
515 int albumSize = (int) mContext.getResources().getDimension(
516 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400517 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500518 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
519 roundedDrawable.setCornerRadius(radius);
520 } else {
521 Log.e(TAG, "No album art available");
522 }
523
524 // Now that it's resized, update the UI
525 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400526 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500527 if (result != null) {
528 albumView.setImageDrawable(result);
529 albumView.setVisibility(View.VISIBLE);
530 } else {
531 albumView.setImageDrawable(null);
532 albumView.setVisibility(View.GONE);
533 }
534 });
535 }
536
537 /**
538 * Update the current device information
539 * @param device device information to display
540 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400541 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500542 if (mSeamless == null) {
543 return;
544 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400545 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500546 updateChipInternal(device);
547 });
548 }
549
550 private void updateChipInternal(MediaDevice device) {
551 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
552
553 // Update the outline color
554 LinearLayout viewLayout = (LinearLayout) mSeamless;
555 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
556 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
557 rect.setStroke(2, mForegroundColor);
558 rect.setColor(mBackgroundColor);
559
560 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
561 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
562 deviceName.setTextColor(fgTintList);
563
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400564 if (mIsRemotePlayback) {
565 mSeamless.setEnabled(false);
566 mSeamless.setAlpha(0.38f);
567 iconView.setImageResource(R.drawable.ic_hardware_speaker);
568 iconView.setVisibility(View.VISIBLE);
569 iconView.setImageTintList(fgTintList);
570 deviceName.setText(R.string.media_seamless_remote_device);
571 } else if (device != null) {
572 mSeamless.setEnabled(true);
573 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500574 Drawable icon = device.getIcon();
575 iconView.setVisibility(View.VISIBLE);
576 iconView.setImageTintList(fgTintList);
577
578 if (icon instanceof AdaptiveIcon) {
579 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
580 aIcon.setBackgroundColor(mBackgroundColor);
581 iconView.setImageDrawable(aIcon);
582 } else {
583 iconView.setImageDrawable(icon);
584 }
585 deviceName.setText(device.getName());
586 } else {
587 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400588 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400589 mSeamless.setEnabled(true);
590 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500591 iconView.setVisibility(View.GONE);
592 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
593 }
594 }
595
596 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400597 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
598 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500599 */
600 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400601 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400602 if (mServiceComponent == null) {
603 // If we don't have a way to resume, just remove the player altogether
604 Log.d(TAG, "Removing unresumable controls");
605 removePlayer();
606 return;
607 }
608 resetButtons();
609 }
610
611 /**
612 * Hide the media buttons and show only a restart button
613 */
614 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500615 // Hide all the old buttons
616 for (int i = 0; i < mActionIds.length; i++) {
617 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
618 if (thisBtn != null) {
619 thisBtn.setVisibility(View.GONE);
620 }
621 }
622
623 // Add a restart button
624 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
625 btn.setOnClickListener(v -> {
626 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400627 if (mQSMediaBrowser != null) {
628 mQSMediaBrowser.disconnect();
629 }
630 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
631 @Override
632 public void onConnected() {
633 Log.d(TAG, "Successfully restarted");
634 }
635 @Override
636 public void onError() {
637 Log.e(TAG, "Error restarting");
638 mQSMediaBrowser.disconnect();
639 mQSMediaBrowser = null;
640 }
641 }, mServiceComponent);
642 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500643 });
644 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
645 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
646 btn.setVisibility(View.VISIBLE);
647 }
648
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400649 private void makeActive() {
650 Assert.isMainThread();
651 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400652 if (mLocalMediaManager != null) {
653 mLocalMediaManager.registerCallback(mDeviceCallback);
654 mLocalMediaManager.startScan();
655 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400656 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500657 }
658 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400659
660 private void makeInactive() {
661 Assert.isMainThread();
662 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400663 if (mLocalMediaManager != null) {
664 mLocalMediaManager.stopScan();
665 mLocalMediaManager.unregisterCallback(mDeviceCallback);
666 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400667 mIsRegistered = false;
668 }
669 }
670
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400671 /**
672 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
673 * component to the list of resumption components
674 */
675 private void tryUpdateResumptionList(ComponentName componentName) {
676 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400677 if (mQSMediaBrowser != null) {
678 mQSMediaBrowser.disconnect();
679 }
680 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400681 new QSMediaBrowser.Callback() {
682 @Override
683 public void onConnected() {
684 Log.d(TAG, "yes we can resume with " + componentName);
685 mServiceComponent = componentName;
686 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400687 mQSMediaBrowser.disconnect();
688 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400689 }
690
691 @Override
692 public void onError() {
693 Log.d(TAG, "Cannot resume with " + componentName);
694 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400695 if (!hasMediaSession()) {
696 // If it's not active and we can't resume, remove
697 removePlayer();
698 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400699 mQSMediaBrowser.disconnect();
700 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400701 }
702 },
703 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400704 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400705 }
706
707 /**
708 * Add the component to the saved list of media browser services, checking for duplicates and
709 * removing older components that exceed the maximum limit
710 * @param componentName
711 */
712 private synchronized void updateResumptionList(ComponentName componentName) {
713 // Add to front of saved list
714 if (mSharedPrefs == null) {
715 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
716 }
717 String componentString = componentName.flattenToString();
718 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
719 if (listString == null) {
720 listString = componentString;
721 } else {
722 String[] components = listString.split(QSMediaBrowser.DELIMITER);
723 StringBuilder updated = new StringBuilder(componentString);
724 int nBrowsers = 1;
725 for (int i = 0; i < components.length
726 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
727 if (componentString.equals(components[i])) {
728 continue;
729 }
730 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
731 nBrowsers++;
732 }
733 listString = updated.toString();
734 }
735 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
736 }
737
738 /**
739 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
740 */
741 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500742}