blob: ddc9c9d7c31486fc138c5897b6b484f6a73ef83b [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;
39import android.media.session.MediaSession;
40import android.media.session.PlaybackState;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040041import android.net.Uri;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040042import android.service.media.MediaBrowserService;
Beth Thibodeaudba74bc2020-04-27 14:17:08 -040043import android.text.TextUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050044import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050045import android.view.LayoutInflater;
46import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040047import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050048import android.view.ViewGroup;
49import android.widget.ImageButton;
50import android.widget.ImageView;
51import android.widget.LinearLayout;
52import android.widget.TextView;
53
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040054import androidx.annotation.Nullable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050055import androidx.core.graphics.drawable.RoundedBitmapDrawable;
56import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
57
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040058import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050059import com.android.settingslib.media.MediaDevice;
60import com.android.settingslib.media.MediaOutputSliceConstants;
61import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050062import com.android.systemui.R;
63import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040064import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040065import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050066
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040067import java.io.IOException;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050068import java.util.List;
69import java.util.concurrent.Executor;
70
71/**
72 * Base media control panel for System UI
73 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040074public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050075 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040076 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040077 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040078 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040079 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050080
81 private Context mContext;
82 protected LinearLayout mMediaNotifView;
83 private View mSeamless;
84 private MediaSession.Token mToken;
85 private MediaController mController;
86 private int mForegroundColor;
87 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040088 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040089 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040090 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040091 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050092
93 private final int[] mActionIds;
94
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040095 public static final String MEDIA_PREFERENCES = "media_control_prefs";
96 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
97 private SharedPreferences mSharedPrefs;
98 private boolean mCheckedForResumption = false;
99
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500100 // Button IDs used in notifications
101 protected static final int[] NOTIF_ACTION_IDS = {
102 com.android.internal.R.id.action0,
103 com.android.internal.R.id.action1,
104 com.android.internal.R.id.action2,
105 com.android.internal.R.id.action3,
106 com.android.internal.R.id.action4
107 };
108
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400109 // URI fields to try loading album art from
110 private static final String[] ART_URIS = {
111 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
112 MediaMetadata.METADATA_KEY_ART_URI,
113 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
114 };
115
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400116 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500117 @Override
118 public void onSessionDestroyed() {
119 Log.d(TAG, "session destroyed");
120 mController.unregisterCallback(mSessionCallback);
121 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400122 makeInactive();
123 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400124 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400125 public void onPlaybackStateChanged(PlaybackState state) {
126 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
127 // When the playback state is NONE or CONNECTING, transition the player to the
128 // resumption state. State CONNECTING needs to be considered for Cast sessions. Ending
129 // a cast session in YT results in the CONNECTING state, which makes sense if you
130 // thinking of the session as waiting to connect to another cast device.
131 if (s == PlaybackState.STATE_NONE || s == PlaybackState.STATE_CONNECTING) {
132 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);
191 mMediaNotifView = (LinearLayout) 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)) {
237 mToken = token;
238 mServiceComponent = null;
239 mCheckedForResumption = false;
240 }
241
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500242 mForegroundColor = iconColor;
243 mBackgroundColor = bgColor;
244 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400245 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500246
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400247 // Try to find a browser service component for this app
248 // TODO also check for a media button receiver intended for restarting (b/154127084)
249 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400250 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400251 if (mServiceComponent == null && !mCheckedForResumption) {
252 Log.d(TAG, "Checking for service component");
253 PackageManager pm = mContext.getPackageManager();
254 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
255 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
256 if (resumeInfo != null) {
257 for (ResolveInfo inf : resumeInfo) {
258 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
259 mBackgroundExecutor.execute(() ->
260 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
261 break;
262 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500263 }
264 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400265 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500266 }
267
268 mController.registerCallback(mSessionCallback);
269
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500270 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
271
272 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400273 if (contentIntent != null) {
274 mMediaNotifView.setOnClickListener(v -> {
Beth Thibodeaue561c002020-04-23 17:33:00 -0400275 mActivityStarter.postStartActivityDismissingKeyguard(contentIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400276 });
277 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500278
279 // App icon
280 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500281 iconDrawable.setTint(mForegroundColor);
282 appIcon.setImageDrawable(iconDrawable);
283
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500284 // Transfer chip
285 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400286 if (mSeamless != null) {
287 if (mLocalMediaManager != null) {
288 mSeamless.setVisibility(View.VISIBLE);
289 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
290 mSeamless.setOnClickListener(v -> {
291 final Intent intent = new Intent()
292 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
293 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
294 mController.getPackageName())
295 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
296 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
297 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
298 });
299 } else {
300 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
301 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500302 }
303
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400304 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400305
306 // App title (not in mini player)
307 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
308 if (appName != null) {
309 appName.setText(appNameString);
310 appName.setTextColor(mForegroundColor);
311 }
312
313 MediaMetadata mediaMetadata = mController.getMetadata();
314 if (mediaMetadata == null) {
315 Log.e(TAG, "Media metadata was null");
316 return;
317 }
318
319 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
320 if (albumView != null) {
321 // Resize art in a background thread
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400322 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400323 }
324
325 // Song name
326 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
327 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
328 titleText.setText(songName);
329 titleText.setTextColor(mForegroundColor);
330
331 // Artist name (not in mini player)
332 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
333 if (artistText != null) {
334 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
335 artistText.setText(artistName);
336 artistText.setTextColor(mForegroundColor);
337 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500338 }
339
340 /**
341 * Return the token for the current media session
342 * @return the token
343 */
344 public MediaSession.Token getMediaSessionToken() {
345 return mToken;
346 }
347
348 /**
349 * Get the current media controller
350 * @return the controller
351 */
352 public MediaController getController() {
353 return mController;
354 }
355
356 /**
357 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400358 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500359 */
360 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400361 if (mController == null) {
362 return null;
363 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500364 return mController.getPackageName();
365 }
366
367 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400368 * Return the original notification's key
369 * @return The notification key
370 */
371 public String getKey() {
372 return mKey;
373 }
374
375 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500376 * Check whether this player has an attached media session.
377 * @return whether there is a controller with a current media session.
378 */
379 public boolean hasMediaSession() {
380 return mController != null && mController.getPlaybackState() != null;
381 }
382
383 /**
384 * Check whether the media controlled by this player is currently playing
385 * @return whether it is playing, or false if no controller information
386 */
387 public boolean isPlaying() {
388 return isPlaying(mController);
389 }
390
391 /**
392 * Check whether the given controller is currently playing
393 * @param controller media controller to check
394 * @return whether it is playing, or false if no controller information
395 */
396 protected boolean isPlaying(MediaController controller) {
397 if (controller == null) {
398 return false;
399 }
400
401 PlaybackState state = controller.getPlaybackState();
402 if (state == null) {
403 return false;
404 }
405
406 return (state.getState() == PlaybackState.STATE_PLAYING);
407 }
408
409 /**
410 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400411 * @param description media description
412 * @param albumView view to hold the album art
413 */
414 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400415 Bitmap albumArt = null;
416
417 // First try loading from URI
418 albumArt = loadBitmapFromUri(description.getIconUri());
419
420 // Then check bitmap
421 if (albumArt == null) {
422 albumArt = description.getIconBitmap();
423 }
424
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400425 processAlbumArtInternal(albumArt, albumView);
426 }
427
428 /**
429 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500430 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400431 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500432 * @param albumView view to hold the album art
433 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400434 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
435 Bitmap albumArt = null;
436
437 // First look in URI fields
438 for (String field : ART_URIS) {
439 String uriString = metadata.getString(field);
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400440 if (!TextUtils.isEmpty(uriString)) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400441 albumArt = loadBitmapFromUri(Uri.parse(uriString));
442 if (albumArt != null) {
443 Log.d(TAG, "loaded art from " + field);
444 break;
445 }
446 }
447 }
448
449 // Then check bitmap field
450 if (albumArt == null) {
451 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
452 }
453
454 // Finally try the notification's largeIcon
455 if (albumArt == null && largeIcon != null) {
456 albumArt = largeIcon.getBitmap();
457 }
458
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400459 processAlbumArtInternal(albumArt, albumView);
460 }
461
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400462 /**
463 * Load a bitmap from a URI
464 * @param uri
465 * @return bitmap, or null if couldn't be loaded
466 */
467 private Bitmap loadBitmapFromUri(Uri uri) {
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400468 // ImageDecoder requires a scheme of the following types
469 if (uri.getScheme() == null) {
470 return null;
471 }
472
473 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
474 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
475 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
476 return null;
477 }
478
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400479 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
480 try {
481 return ImageDecoder.decodeBitmap(source);
482 } catch (IOException e) {
483 e.printStackTrace();
484 return null;
485 }
486 }
487
488 /**
489 * Resize and crop the image if provided and update the control view
490 * @param albumArt Bitmap of art to display, or null to hide view
491 * @param albumView View that will hold the art
492 */
493 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
494 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500495 RoundedBitmapDrawable roundedDrawable = null;
496 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400497 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500498 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
499 int albumSize = (int) mContext.getResources().getDimension(
500 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400501 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500502 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
503 roundedDrawable.setCornerRadius(radius);
504 } else {
505 Log.e(TAG, "No album art available");
506 }
507
508 // Now that it's resized, update the UI
509 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400510 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500511 if (result != null) {
512 albumView.setImageDrawable(result);
513 albumView.setVisibility(View.VISIBLE);
514 } else {
515 albumView.setImageDrawable(null);
516 albumView.setVisibility(View.GONE);
517 }
518 });
519 }
520
521 /**
522 * Update the current device information
523 * @param device device information to display
524 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400525 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500526 if (mSeamless == null) {
527 return;
528 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400529 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500530 updateChipInternal(device);
531 });
532 }
533
534 private void updateChipInternal(MediaDevice device) {
535 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
536
537 // Update the outline color
538 LinearLayout viewLayout = (LinearLayout) mSeamless;
539 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
540 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
541 rect.setStroke(2, mForegroundColor);
542 rect.setColor(mBackgroundColor);
543
544 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
545 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
546 deviceName.setTextColor(fgTintList);
547
548 if (device != null) {
549 Drawable icon = device.getIcon();
550 iconView.setVisibility(View.VISIBLE);
551 iconView.setImageTintList(fgTintList);
552
553 if (icon instanceof AdaptiveIcon) {
554 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
555 aIcon.setBackgroundColor(mBackgroundColor);
556 iconView.setImageDrawable(aIcon);
557 } else {
558 iconView.setImageDrawable(icon);
559 }
560 deviceName.setText(device.getName());
561 } else {
562 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400563 Log.d(TAG, "device is null. Not binding output chip.");
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500564 iconView.setVisibility(View.GONE);
565 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
566 }
567 }
568
569 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400570 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
571 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500572 */
573 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400574 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400575 if (mServiceComponent == null) {
576 // If we don't have a way to resume, just remove the player altogether
577 Log.d(TAG, "Removing unresumable controls");
578 removePlayer();
579 return;
580 }
581 resetButtons();
582 }
583
584 /**
585 * Hide the media buttons and show only a restart button
586 */
587 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500588 // Hide all the old buttons
589 for (int i = 0; i < mActionIds.length; i++) {
590 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
591 if (thisBtn != null) {
592 thisBtn.setVisibility(View.GONE);
593 }
594 }
595
596 // Add a restart button
597 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
598 btn.setOnClickListener(v -> {
599 Log.d(TAG, "Attempting to restart session");
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400600 QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
601 browser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500602 });
603 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
604 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
605 btn.setVisibility(View.VISIBLE);
606 }
607
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400608 private void makeActive() {
609 Assert.isMainThread();
610 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400611 if (mLocalMediaManager != null) {
612 mLocalMediaManager.registerCallback(mDeviceCallback);
613 mLocalMediaManager.startScan();
614 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400615 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500616 }
617 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400618
619 private void makeInactive() {
620 Assert.isMainThread();
621 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400622 if (mLocalMediaManager != null) {
623 mLocalMediaManager.stopScan();
624 mLocalMediaManager.unregisterCallback(mDeviceCallback);
625 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400626 mIsRegistered = false;
627 }
628 }
629
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400630 /**
631 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
632 * component to the list of resumption components
633 */
634 private void tryUpdateResumptionList(ComponentName componentName) {
635 Log.d(TAG, "Testing if we can connect to " + componentName);
636 QSMediaBrowser.testConnection(mContext,
637 new QSMediaBrowser.Callback() {
638 @Override
639 public void onConnected() {
640 Log.d(TAG, "yes we can resume with " + componentName);
641 mServiceComponent = componentName;
642 updateResumptionList(componentName);
643 }
644
645 @Override
646 public void onError() {
647 Log.d(TAG, "Cannot resume with " + componentName);
648 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400649 if (!hasMediaSession()) {
650 // If it's not active and we can't resume, remove
651 removePlayer();
652 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400653 }
654 },
655 componentName);
656 }
657
658 /**
659 * Add the component to the saved list of media browser services, checking for duplicates and
660 * removing older components that exceed the maximum limit
661 * @param componentName
662 */
663 private synchronized void updateResumptionList(ComponentName componentName) {
664 // Add to front of saved list
665 if (mSharedPrefs == null) {
666 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
667 }
668 String componentString = componentName.flattenToString();
669 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
670 if (listString == null) {
671 listString = componentString;
672 } else {
673 String[] components = listString.split(QSMediaBrowser.DELIMITER);
674 StringBuilder updated = new StringBuilder(componentString);
675 int nBrowsers = 1;
676 for (int i = 0; i < components.length
677 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
678 if (componentString.equals(components[i])) {
679 continue;
680 }
681 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
682 nBrowsers++;
683 }
684 listString = updated.toString();
685 }
686 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
687 }
688
689 /**
690 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
691 */
692 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500693}