blob: 8cb0a4ea7a5c13632e94b8ae79d276a88e967a9e [file] [log] [blame]
Beth Thibodeau7b6c1782020-03-05 11:43:51 -05001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.media;
18
19import android.annotation.LayoutRes;
20import android.app.PendingIntent;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040024import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050025import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.ColorStateList;
28import android.graphics.Bitmap;
29import android.graphics.drawable.Drawable;
30import android.graphics.drawable.GradientDrawable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050031import android.graphics.drawable.RippleDrawable;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040032import android.media.MediaDescription;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050033import android.media.MediaMetadata;
34import android.media.session.MediaController;
35import android.media.session.MediaSession;
36import android.media.session.PlaybackState;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040037import android.service.media.MediaBrowserService;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050038import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050039import android.view.LayoutInflater;
40import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040041import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050042import android.view.ViewGroup;
43import android.widget.ImageButton;
44import android.widget.ImageView;
45import android.widget.LinearLayout;
46import android.widget.TextView;
47
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040048import androidx.annotation.Nullable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050049import androidx.core.graphics.drawable.RoundedBitmapDrawable;
50import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
51
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040052import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050053import com.android.settingslib.media.MediaDevice;
54import com.android.settingslib.media.MediaOutputSliceConstants;
55import com.android.settingslib.widget.AdaptiveIcon;
56import com.android.systemui.Dependency;
57import com.android.systemui.R;
58import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040059import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040060import com.android.systemui.util.Assert;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050061
62import java.util.List;
63import java.util.concurrent.Executor;
64
65/**
66 * Base media control panel for System UI
67 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040068public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050069 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040070 @Nullable private final LocalMediaManager mLocalMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040071 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040072 protected final Executor mBackgroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050073
74 private Context mContext;
75 protected LinearLayout mMediaNotifView;
76 private View mSeamless;
77 private MediaSession.Token mToken;
78 private MediaController mController;
79 private int mForegroundColor;
80 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040081 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040082 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040083 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -040084 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050085
86 private final int[] mActionIds;
87
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040088 public static final String MEDIA_PREFERENCES = "media_control_prefs";
89 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
90 private SharedPreferences mSharedPrefs;
91 private boolean mCheckedForResumption = false;
92
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050093 // Button IDs used in notifications
94 protected static final int[] NOTIF_ACTION_IDS = {
95 com.android.internal.R.id.action0,
96 com.android.internal.R.id.action1,
97 com.android.internal.R.id.action2,
98 com.android.internal.R.id.action3,
99 com.android.internal.R.id.action4
100 };
101
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400102 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500103 @Override
104 public void onSessionDestroyed() {
105 Log.d(TAG, "session destroyed");
106 mController.unregisterCallback(mSessionCallback);
107 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400108 makeInactive();
109 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400110 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400111 public void onPlaybackStateChanged(PlaybackState state) {
112 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
113 // When the playback state is NONE or CONNECTING, transition the player to the
114 // resumption state. State CONNECTING needs to be considered for Cast sessions. Ending
115 // a cast session in YT results in the CONNECTING state, which makes sense if you
116 // thinking of the session as waiting to connect to another cast device.
117 if (s == PlaybackState.STATE_NONE || s == PlaybackState.STATE_CONNECTING) {
118 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400119 clearControls();
120 makeInactive();
121 }
122 }
123 };
124
125 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
126 @Override
127 public void onViewAttachedToWindow(View unused) {
128 makeActive();
129 }
130 @Override
131 public void onViewDetachedFromWindow(View unused) {
132 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500133 }
134 };
135
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400136 private final LocalMediaManager.DeviceCallback mDeviceCallback =
137 new LocalMediaManager.DeviceCallback() {
138 @Override
139 public void onDeviceListUpdate(List<MediaDevice> devices) {
140 if (mLocalMediaManager == null) {
141 return;
142 }
143 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
144 // Check because this can be called several times while changing devices
145 if (mDevice == null || !mDevice.equals(currentDevice)) {
146 mDevice = currentDevice;
147 updateDevice(mDevice);
148 }
149 }
150
151 @Override
152 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
153 if (mDevice == null || !mDevice.equals(device)) {
154 mDevice = device;
155 updateDevice(mDevice);
156 }
157 }
158 };
159
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500160 /**
161 * Initialize a new control panel
162 * @param context
163 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400164 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500165 * @param layoutId layout resource to use for this control panel
166 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400167 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500168 * @param backgroundExecutor background executor, used for processing artwork
169 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400170 public MediaControlPanel(Context context, ViewGroup parent,
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400171 @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
172 Executor foregroundExecutor, Executor backgroundExecutor) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500173 mContext = context;
174 LayoutInflater inflater = LayoutInflater.from(mContext);
175 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400176 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
177 // mStateListener shouldn't need to be unregistered since this object shares the same
178 // lifecycle with the inflated view. It would be better, however, if this controller used an
179 // attach/detach of views instead of inflating them in the constructor, which would allow
180 // mStateListener to be unregistered in detach.
181 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400182 mLocalMediaManager = routeManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500183 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400184 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500185 mBackgroundExecutor = backgroundExecutor;
186 }
187
188 /**
189 * Get the view used to display media controls
190 * @return the view
191 */
192 public View getView() {
193 return mMediaNotifView;
194 }
195
196 /**
197 * Get the context
198 * @return context
199 */
200 public Context getContext() {
201 return mContext;
202 }
203
204 /**
205 * Update the media panel view for the given media session
206 * @param token
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400207 * @param iconDrawable
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500208 * @param iconColor
209 * @param bgColor
210 * @param contentIntent
211 * @param appNameString
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400212 * @param key
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500213 */
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400214 public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, int iconColor,
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400215 int bgColor, PendingIntent contentIntent, String appNameString, String key) {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400216 // Ensure that component names are updated if token has changed
217 if (mToken == null || !mToken.equals(token)) {
218 mToken = token;
219 mServiceComponent = null;
220 mCheckedForResumption = false;
221 }
222
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500223 mForegroundColor = iconColor;
224 mBackgroundColor = bgColor;
225 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400226 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500227
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400228 // Try to find a browser service component for this app
229 // TODO also check for a media button receiver intended for restarting (b/154127084)
230 // Only check if we haven't tried yet or the session token changed
231 String pkgName = mController.getPackageName();
232 if (mServiceComponent == null && !mCheckedForResumption) {
233 Log.d(TAG, "Checking for service component");
234 PackageManager pm = mContext.getPackageManager();
235 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
236 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
237 if (resumeInfo != null) {
238 for (ResolveInfo inf : resumeInfo) {
239 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
240 mBackgroundExecutor.execute(() ->
241 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
242 break;
243 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500244 }
245 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400246 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500247 }
248
249 mController.registerCallback(mSessionCallback);
250
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500251 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
252
253 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400254 if (contentIntent != null) {
255 mMediaNotifView.setOnClickListener(v -> {
256 try {
257 contentIntent.send();
258 // Also close shade
259 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
260 } catch (PendingIntent.CanceledException e) {
261 Log.e(TAG, "Pending intent was canceled", e);
262 }
263 });
264 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500265
266 // App icon
267 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500268 iconDrawable.setTint(mForegroundColor);
269 appIcon.setImageDrawable(iconDrawable);
270
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500271 // Transfer chip
272 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400273 if (mSeamless != null && mLocalMediaManager != null) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500274 mSeamless.setVisibility(View.VISIBLE);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400275 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500276 ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
277 mSeamless.setOnClickListener(v -> {
278 final Intent intent = new Intent()
279 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
280 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
281 mController.getPackageName())
282 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
283 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
284 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
285 });
286 }
287
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400288 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400289
290 // App title (not in mini player)
291 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
292 if (appName != null) {
293 appName.setText(appNameString);
294 appName.setTextColor(mForegroundColor);
295 }
296
297 MediaMetadata mediaMetadata = mController.getMetadata();
298 if (mediaMetadata == null) {
299 Log.e(TAG, "Media metadata was null");
300 return;
301 }
302
303 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
304 if (albumView != null) {
305 // Resize art in a background thread
306 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
307 }
308
309 // Song name
310 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
311 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
312 titleText.setText(songName);
313 titleText.setTextColor(mForegroundColor);
314
315 // Artist name (not in mini player)
316 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
317 if (artistText != null) {
318 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
319 artistText.setText(artistName);
320 artistText.setTextColor(mForegroundColor);
321 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500322 }
323
324 /**
325 * Return the token for the current media session
326 * @return the token
327 */
328 public MediaSession.Token getMediaSessionToken() {
329 return mToken;
330 }
331
332 /**
333 * Get the current media controller
334 * @return the controller
335 */
336 public MediaController getController() {
337 return mController;
338 }
339
340 /**
341 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400342 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500343 */
344 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400345 if (mController == null) {
346 return null;
347 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500348 return mController.getPackageName();
349 }
350
351 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400352 * Return the original notification's key
353 * @return The notification key
354 */
355 public String getKey() {
356 return mKey;
357 }
358
359 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500360 * Check whether this player has an attached media session.
361 * @return whether there is a controller with a current media session.
362 */
363 public boolean hasMediaSession() {
364 return mController != null && mController.getPlaybackState() != null;
365 }
366
367 /**
368 * Check whether the media controlled by this player is currently playing
369 * @return whether it is playing, or false if no controller information
370 */
371 public boolean isPlaying() {
372 return isPlaying(mController);
373 }
374
375 /**
376 * Check whether the given controller is currently playing
377 * @param controller media controller to check
378 * @return whether it is playing, or false if no controller information
379 */
380 protected boolean isPlaying(MediaController controller) {
381 if (controller == null) {
382 return false;
383 }
384
385 PlaybackState state = controller.getPlaybackState();
386 if (state == null) {
387 return false;
388 }
389
390 return (state.getState() == PlaybackState.STATE_PLAYING);
391 }
392
393 /**
394 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400395 * @param description media description
396 * @param albumView view to hold the album art
397 */
398 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
399 Bitmap albumArt = description.getIconBitmap();
400 //TODO check other fields (b/151054111, b/152067055)
401 processAlbumArtInternal(albumArt, albumView);
402 }
403
404 /**
405 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500406 * @param metadata media metadata
407 * @param albumView view to hold the album art
408 */
409 private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
410 Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400411 //TODO check other fields (b/151054111, b/152067055)
412 processAlbumArtInternal(albumArt, albumView);
413 }
414
415 private void processAlbumArtInternal(Bitmap albumArt, ImageView albumView) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500416 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
417 RoundedBitmapDrawable roundedDrawable = null;
418 if (albumArt != null) {
419 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
420 int albumSize = (int) mContext.getResources().getDimension(
421 R.dimen.qs_media_album_size);
422 Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
423 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
424 roundedDrawable.setCornerRadius(radius);
425 } else {
426 Log.e(TAG, "No album art available");
427 }
428
429 // Now that it's resized, update the UI
430 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400431 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500432 if (result != null) {
433 albumView.setImageDrawable(result);
434 albumView.setVisibility(View.VISIBLE);
435 } else {
436 albumView.setImageDrawable(null);
437 albumView.setVisibility(View.GONE);
438 }
439 });
440 }
441
442 /**
443 * Update the current device information
444 * @param device device information to display
445 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400446 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500447 if (mSeamless == null) {
448 return;
449 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400450 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500451 updateChipInternal(device);
452 });
453 }
454
455 private void updateChipInternal(MediaDevice device) {
456 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
457
458 // Update the outline color
459 LinearLayout viewLayout = (LinearLayout) mSeamless;
460 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
461 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
462 rect.setStroke(2, mForegroundColor);
463 rect.setColor(mBackgroundColor);
464
465 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
466 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
467 deviceName.setTextColor(fgTintList);
468
469 if (device != null) {
470 Drawable icon = device.getIcon();
471 iconView.setVisibility(View.VISIBLE);
472 iconView.setImageTintList(fgTintList);
473
474 if (icon instanceof AdaptiveIcon) {
475 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
476 aIcon.setBackgroundColor(mBackgroundColor);
477 iconView.setImageDrawable(aIcon);
478 } else {
479 iconView.setImageDrawable(icon);
480 }
481 deviceName.setText(device.getName());
482 } else {
483 // Reset to default
484 iconView.setVisibility(View.GONE);
485 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
486 }
487 }
488
489 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400490 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
491 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500492 */
493 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400494 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400495 if (mServiceComponent == null) {
496 // If we don't have a way to resume, just remove the player altogether
497 Log.d(TAG, "Removing unresumable controls");
498 removePlayer();
499 return;
500 }
501 resetButtons();
502 }
503
504 /**
505 * Hide the media buttons and show only a restart button
506 */
507 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500508 // Hide all the old buttons
509 for (int i = 0; i < mActionIds.length; i++) {
510 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
511 if (thisBtn != null) {
512 thisBtn.setVisibility(View.GONE);
513 }
514 }
515
516 // Add a restart button
517 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
518 btn.setOnClickListener(v -> {
519 Log.d(TAG, "Attempting to restart session");
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400520 QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
521 browser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500522 });
523 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
524 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
525 btn.setVisibility(View.VISIBLE);
526 }
527
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400528 private void makeActive() {
529 Assert.isMainThread();
530 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400531 if (mLocalMediaManager != null) {
532 mLocalMediaManager.registerCallback(mDeviceCallback);
533 mLocalMediaManager.startScan();
534 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400535 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500536 }
537 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400538
539 private void makeInactive() {
540 Assert.isMainThread();
541 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400542 if (mLocalMediaManager != null) {
543 mLocalMediaManager.stopScan();
544 mLocalMediaManager.unregisterCallback(mDeviceCallback);
545 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400546 mIsRegistered = false;
547 }
548 }
549
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400550 /**
551 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
552 * component to the list of resumption components
553 */
554 private void tryUpdateResumptionList(ComponentName componentName) {
555 Log.d(TAG, "Testing if we can connect to " + componentName);
556 QSMediaBrowser.testConnection(mContext,
557 new QSMediaBrowser.Callback() {
558 @Override
559 public void onConnected() {
560 Log.d(TAG, "yes we can resume with " + componentName);
561 mServiceComponent = componentName;
562 updateResumptionList(componentName);
563 }
564
565 @Override
566 public void onError() {
567 Log.d(TAG, "Cannot resume with " + componentName);
568 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400569 if (!hasMediaSession()) {
570 // If it's not active and we can't resume, remove
571 removePlayer();
572 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400573 }
574 },
575 componentName);
576 }
577
578 /**
579 * Add the component to the saved list of media browser services, checking for duplicates and
580 * removing older components that exceed the maximum limit
581 * @param componentName
582 */
583 private synchronized void updateResumptionList(ComponentName componentName) {
584 // Add to front of saved list
585 if (mSharedPrefs == null) {
586 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
587 }
588 String componentString = componentName.flattenToString();
589 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
590 if (listString == null) {
591 listString = componentString;
592 } else {
593 String[] components = listString.split(QSMediaBrowser.DELIMITER);
594 StringBuilder updated = new StringBuilder(componentString);
595 int nBrowsers = 1;
596 for (int i = 0; i < components.length
597 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
598 if (componentString.equals(components[i])) {
599 continue;
600 }
601 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
602 nBrowsers++;
603 }
604 listString = updated.toString();
605 }
606 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
607 }
608
609 /**
610 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
611 */
612 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500613}