blob: 0297c90a29b9c10e5f64b56406b39cf5b928caa9 [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
Selim Cinek5dbef2d2020-05-07 17:44:38 -070019import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
20
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050021import android.annotation.LayoutRes;
22import android.app.PendingIntent;
23import android.content.ComponentName;
Beth Thibodeaudba74bc2020-04-27 14:17:08 -040024import android.content.ContentResolver;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050025import android.content.Context;
26import android.content.Intent;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040027import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050028import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.res.ColorStateList;
31import android.graphics.Bitmap;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040032import android.graphics.ImageDecoder;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070033import android.graphics.Rect;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050034import android.graphics.drawable.Drawable;
35import android.graphics.drawable.GradientDrawable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.graphics.drawable.RippleDrawable;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040037import android.media.MediaDescription;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050038import android.media.MediaMetadata;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040039import android.media.ThumbnailUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050040import android.media.session.MediaController;
Robert Snoebergerc981dc92020-04-27 15:00:50 -040041import android.media.session.MediaController.PlaybackInfo;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050042import android.media.session.MediaSession;
43import android.media.session.PlaybackState;
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040044import android.net.Uri;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040045import android.service.media.MediaBrowserService;
Beth Thibodeaudba74bc2020-04-27 14:17:08 -040046import android.text.TextUtils;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050047import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050048import android.view.LayoutInflater;
49import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040050import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050051import android.view.ViewGroup;
52import android.widget.ImageButton;
53import android.widget.ImageView;
54import android.widget.LinearLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070055import android.widget.SeekBar;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050056import android.widget.TextView;
57
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040058import androidx.annotation.Nullable;
Selim Cinekd8357922020-04-10 15:06:53 -070059import androidx.constraintlayout.motion.widget.MotionLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070060import androidx.constraintlayout.widget.ConstraintSet;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050061import androidx.core.graphics.drawable.RoundedBitmapDrawable;
62import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
63
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040064import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050065import com.android.settingslib.media.MediaDevice;
66import com.android.settingslib.media.MediaOutputSliceConstants;
67import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050068import com.android.systemui.R;
69import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040070import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040071import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070072import com.android.systemui.util.concurrency.DelayableExecutor;
73
74import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050075
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -040076import java.io.IOException;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050077import java.util.List;
78import java.util.concurrent.Executor;
79
80/**
Selim Cinekd8357922020-04-10 15:06:53 -070081 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050082 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040083public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050084 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040085 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070086
87 // Button IDs for QS controls
88 static final int[] ACTION_IDS = {
89 R.id.action0,
90 R.id.action1,
91 R.id.action2,
92 R.id.action3,
93 R.id.action4
94 };
95
96 private final SeekBarViewModel mSeekBarViewModel;
97 private final SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040098 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040099 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400100 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500101
102 private Context mContext;
Selim Cinekd8357922020-04-10 15:06:53 -0700103 protected MotionLayout mMediaNotifView;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500104 private View mSeamless;
105 private MediaSession.Token mToken;
106 private MediaController mController;
107 private int mForegroundColor;
108 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400109 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400110 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400111 private boolean mIsRegistered = false;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400112 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500113
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400114 public static final String MEDIA_PREFERENCES = "media_control_prefs";
115 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
116 private SharedPreferences mSharedPrefs;
117 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400118 private boolean mIsRemotePlayback;
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400119 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400120
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400121 // URI fields to try loading album art from
122 private static final String[] ART_URIS = {
123 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
124 MediaMetadata.METADATA_KEY_ART_URI,
125 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
126 };
127
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400128 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500129 @Override
130 public void onSessionDestroyed() {
131 Log.d(TAG, "session destroyed");
132 mController.unregisterCallback(mSessionCallback);
133 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400134 makeInactive();
135 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400136 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400137 public void onPlaybackStateChanged(PlaybackState state) {
138 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400139 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400140 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400141 clearControls();
142 makeInactive();
143 }
144 }
145 };
146
147 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
148 @Override
149 public void onViewAttachedToWindow(View unused) {
150 makeActive();
151 }
152 @Override
153 public void onViewDetachedFromWindow(View unused) {
154 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500155 }
156 };
157
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400158 private final LocalMediaManager.DeviceCallback mDeviceCallback =
159 new LocalMediaManager.DeviceCallback() {
160 @Override
161 public void onDeviceListUpdate(List<MediaDevice> devices) {
162 if (mLocalMediaManager == null) {
163 return;
164 }
165 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
166 // Check because this can be called several times while changing devices
167 if (mDevice == null || !mDevice.equals(currentDevice)) {
168 mDevice = currentDevice;
169 updateDevice(mDevice);
170 }
171 }
172
173 @Override
174 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
175 if (mDevice == null || !mDevice.equals(device)) {
176 mDevice = device;
177 updateDevice(mDevice);
178 }
179 }
180 };
181
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500182 /**
183 * Initialize a new control panel
184 * @param context
185 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400186 * @param routeManager Manager used to listen for device change events.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500187 * @param layoutId layout resource to use for this control panel
188 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400189 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500190 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400191 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500192 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400193 public MediaControlPanel(Context context, ViewGroup parent,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700194 @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
195 DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500196 mContext = context;
197 LayoutInflater inflater = LayoutInflater.from(mContext);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700198 mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400199 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
200 // mStateListener shouldn't need to be unregistered since this object shares the same
201 // lifecycle with the inflated view. It would be better, however, if this controller used an
202 // attach/detach of views instead of inflating them in the constructor, which would allow
203 // mStateListener to be unregistered in detach.
204 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400205 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400206 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500207 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400208 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700209 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
210 mSeekBarObserver = new SeekBarObserver(getView());
211 // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
212 // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
213 // will stop updating. So, use the lifecycle of the parent instead.
214 // TODO: this parent is also detached, need to fix that
215 mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
216 SeekBar bar = getView().findViewById(R.id.media_progress_bar);
217 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
218 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500219 }
220
221 /**
222 * Get the view used to display media controls
223 * @return the view
224 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700225 public MotionLayout getView() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500226 return mMediaNotifView;
227 }
228
229 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700230 * Sets the listening state of the player.
231 *
232 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
233 * unnecessary work when the QS panel is closed.
234 *
235 * @param listening True when player should be active. Otherwise, false.
236 */
237 public void setListening(boolean listening) {
238 mSeekBarViewModel.setListening(listening);
239 }
240
241 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500242 * Get the context
243 * @return context
244 */
245 public Context getContext() {
246 return mContext;
247 }
248
249 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700250 * Bind this view based on the data given
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500251 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700252 public void bind(@NotNull MediaData data) {
253 mToken = data.getToken();
254 mForegroundColor = data.getForegroundColor();
255 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400256 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400257 if (mQSMediaBrowser != null) {
258 Log.d(TAG, "Disconnecting old media browser");
259 mQSMediaBrowser.disconnect();
260 mQSMediaBrowser = null;
261 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400262 mToken = token;
263 mServiceComponent = null;
264 mCheckedForResumption = false;
265 }
266
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500267 mController = new MediaController(mContext, mToken);
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400268 mKey = key;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500269
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400270 // Try to find a browser service component for this app
271 // TODO also check for a media button receiver intended for restarting (b/154127084)
272 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400273 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400274 if (mServiceComponent == null && !mCheckedForResumption) {
275 Log.d(TAG, "Checking for service component");
276 PackageManager pm = mContext.getPackageManager();
277 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
278 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700279 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400280 if (resumeInfo != null) {
281 for (ResolveInfo inf : resumeInfo) {
282 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
283 mBackgroundExecutor.execute(() ->
284 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
285 break;
286 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500287 }
288 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400289 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500290 }
291
292 mController.registerCallback(mSessionCallback);
293
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700294 albumView.setImageDrawable(data.getArtwork());
295
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500296 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
297
298 // Click action
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700299 PendingIntent clickIntent = data.getClickIntent();
300 if (clickIntent != null) {
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400301 mMediaNotifView.setOnClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700302 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400303 });
304 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500305
306 // App icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700307 ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
308 // TODO: look at iconDrawable
309 Drawable iconDrawable = data.getAppIcon();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500310 iconDrawable.setTint(mForegroundColor);
311 appIcon.setImageDrawable(iconDrawable);
312
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700313 TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
314 titleText.setText(data.getSong());
315 TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
316 appName.setText(data.getApp());
317 appName.setTextColor(mForegroundColor);
318 TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
319 artistText.setText(data.getArtist());
320 artistText.setTextColor(mForegroundColor);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500321 // Transfer chip
322 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400323 if (mSeamless != null) {
324 if (mLocalMediaManager != null) {
325 mSeamless.setVisibility(View.VISIBLE);
326 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
327 mSeamless.setOnClickListener(v -> {
328 final Intent intent = new Intent()
329 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
330 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
331 mController.getPackageName())
332 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
333 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
334 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
335 });
336 } else {
337 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
338 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500339 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400340 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
341 if (playbackInfo != null) {
342 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
343 } else {
344 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
345 mIsRemotePlayback = false;
346 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500347
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700348 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
349 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
350 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
351 // Media controls
352 int i = 0;
353 List<MediaAction> actionIcons = data.getActions();
354 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
355 final ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
356 MediaAction mediaAction = actionIcons.get(i);
357 button.setImageDrawable(mediaAction.getDrawable());
358 button.setContentDescription(mediaAction.getContentDescription());
359 button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
360 PendingIntent actionIntent = mediaAction.getIntent();
361
362 if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) {
363 ((IlluminationDrawable) mMediaNotifView.getBackground())
364 .setupTouch(button, mMediaNotifView);
365 }
366
367 button.setOnClickListener(v -> {
368 if (actionIntent != null) {
369 try {
370 actionIntent.send();
371 } catch (PendingIntent.CanceledException e) {
372 e.printStackTrace();
373 }
374 }
375 });
376 boolean visibleInCompat = actionsWhenCollapsed.contains(i);
377 collapsedSet.setVisibility(ACTION_IDS[i],
378 visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
379 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.VISIBLE);
380 }
381
382 // Hide any unused buttons
383 for (; i < ACTION_IDS.length; i++) {
384 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
385 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
386 }
387
388 // Seek Bar
389 final MediaController controller = new MediaController(getContext(), data.getToken());
390 mBackgroundExecutor.execute(
391 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
392
393 // Set up long press menu
394 // TODO: b/156036025 bring back media guts
395
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400396 makeActive();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400397
398 // App title (not in mini player)
399 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
400 if (appName != null) {
401 appName.setText(appNameString);
402 appName.setTextColor(mForegroundColor);
403 }
404
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400405 // Can be null!
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400406 MediaMetadata mediaMetadata = mController.getMetadata();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400407
408 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
409 if (albumView != null) {
410 // Resize art in a background thread
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400411 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400412 }
413
414 // Song name
415 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400416 String songName = "";
417 if (mediaMetadata != null) {
418 songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
419 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400420 titleText.setText(songName);
421 titleText.setTextColor(mForegroundColor);
422
423 // Artist name (not in mini player)
424 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
425 if (artistText != null) {
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400426 String artistName = "";
427 if (mediaMetadata != null) {
428 artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
429 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400430 artistText.setText(artistName);
431 artistText.setTextColor(mForegroundColor);
432 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500433 }
434
435 /**
436 * Return the token for the current media session
437 * @return the token
438 */
439 public MediaSession.Token getMediaSessionToken() {
440 return mToken;
441 }
442
443 /**
444 * Get the current media controller
445 * @return the controller
446 */
447 public MediaController getController() {
448 return mController;
449 }
450
451 /**
452 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400453 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500454 */
455 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400456 if (mController == null) {
457 return null;
458 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500459 return mController.getPackageName();
460 }
461
462 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400463 * Return the original notification's key
464 * @return The notification key
465 */
466 public String getKey() {
467 return mKey;
468 }
469
470 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500471 * Check whether this player has an attached media session.
472 * @return whether there is a controller with a current media session.
473 */
474 public boolean hasMediaSession() {
475 return mController != null && mController.getPlaybackState() != null;
476 }
477
478 /**
479 * Check whether the media controlled by this player is currently playing
480 * @return whether it is playing, or false if no controller information
481 */
482 public boolean isPlaying() {
483 return isPlaying(mController);
484 }
485
486 /**
487 * Check whether the given controller is currently playing
488 * @param controller media controller to check
489 * @return whether it is playing, or false if no controller information
490 */
491 protected boolean isPlaying(MediaController controller) {
492 if (controller == null) {
493 return false;
494 }
495
496 PlaybackState state = controller.getPlaybackState();
497 if (state == null) {
498 return false;
499 }
500
501 return (state.getState() == PlaybackState.STATE_PLAYING);
502 }
503
504 /**
505 * Process album art for layout
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400506 * @param description media description
507 * @param albumView view to hold the album art
508 */
509 protected void processAlbumArt(MediaDescription description, ImageView albumView) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400510 Bitmap albumArt = null;
511
512 // First try loading from URI
513 albumArt = loadBitmapFromUri(description.getIconUri());
514
515 // Then check bitmap
516 if (albumArt == null) {
517 albumArt = description.getIconBitmap();
518 }
519
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400520 processAlbumArtInternal(albumArt, albumView);
521 }
522
523 /**
524 * Process album art for layout
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500525 * @param metadata media metadata
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400526 * @param largeIcon from notification, checked as a fallback if metadata does not have art
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500527 * @param albumView view to hold the album art
528 */
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400529 private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
530 Bitmap albumArt = null;
531
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400532 if (metadata != null) {
533 // First look in URI fields
534 for (String field : ART_URIS) {
535 String uriString = metadata.getString(field);
536 if (!TextUtils.isEmpty(uriString)) {
537 albumArt = loadBitmapFromUri(Uri.parse(uriString));
538 if (albumArt != null) {
539 Log.d(TAG, "loaded art from " + field);
540 break;
541 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400542 }
543 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400544
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400545 // Then check bitmap field
546 if (albumArt == null) {
547 albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
548 }
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400549 }
550
551 // Finally try the notification's largeIcon
552 if (albumArt == null && largeIcon != null) {
553 albumArt = largeIcon.getBitmap();
554 }
555
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400556 processAlbumArtInternal(albumArt, albumView);
557 }
558
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400559 /**
560 * Load a bitmap from a URI
561 * @param uri
562 * @return bitmap, or null if couldn't be loaded
563 */
564 private Bitmap loadBitmapFromUri(Uri uri) {
Beth Thibodeaudba74bc2020-04-27 14:17:08 -0400565 // ImageDecoder requires a scheme of the following types
566 if (uri.getScheme() == null) {
567 return null;
568 }
569
570 if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
571 && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
572 && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
573 return null;
574 }
575
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400576 ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
577 try {
578 return ImageDecoder.decodeBitmap(source);
579 } catch (IOException e) {
580 e.printStackTrace();
581 return null;
582 }
583 }
584
585 /**
586 * Resize and crop the image if provided and update the control view
587 * @param albumArt Bitmap of art to display, or null to hide view
588 * @param albumView View that will hold the art
589 */
590 private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
591 // Resize
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500592 RoundedBitmapDrawable roundedDrawable = null;
593 if (albumArt != null) {
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400594 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500595 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
596 int albumSize = (int) mContext.getResources().getDimension(
597 R.dimen.qs_media_album_size);
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400598 Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500599 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
600 roundedDrawable.setCornerRadius(radius);
601 } else {
602 Log.e(TAG, "No album art available");
603 }
604
605 // Now that it's resized, update the UI
606 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400607 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500608 if (result != null) {
609 albumView.setImageDrawable(result);
610 albumView.setVisibility(View.VISIBLE);
611 } else {
612 albumView.setImageDrawable(null);
613 albumView.setVisibility(View.GONE);
614 }
615 });
616 }
617
618 /**
619 * Update the current device information
620 * @param device device information to display
621 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400622 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500623 if (mSeamless == null) {
624 return;
625 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400626 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500627 updateChipInternal(device);
628 });
629 }
630
631 private void updateChipInternal(MediaDevice device) {
632 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
633
634 // Update the outline color
635 LinearLayout viewLayout = (LinearLayout) mSeamless;
636 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
637 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
638 rect.setStroke(2, mForegroundColor);
639 rect.setColor(mBackgroundColor);
640
641 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
642 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
643 deviceName.setTextColor(fgTintList);
644
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400645 if (mIsRemotePlayback) {
646 mSeamless.setEnabled(false);
647 mSeamless.setAlpha(0.38f);
648 iconView.setImageResource(R.drawable.ic_hardware_speaker);
649 iconView.setVisibility(View.VISIBLE);
650 iconView.setImageTintList(fgTintList);
651 deviceName.setText(R.string.media_seamless_remote_device);
652 } else if (device != null) {
653 mSeamless.setEnabled(true);
654 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500655 Drawable icon = device.getIcon();
656 iconView.setVisibility(View.VISIBLE);
657 iconView.setImageTintList(fgTintList);
658
659 if (icon instanceof AdaptiveIcon) {
660 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
661 aIcon.setBackgroundColor(mBackgroundColor);
662 iconView.setImageDrawable(aIcon);
663 } else {
664 iconView.setImageDrawable(icon);
665 }
666 deviceName.setText(device.getName());
667 } else {
668 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400669 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400670 mSeamless.setEnabled(true);
671 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500672 iconView.setVisibility(View.GONE);
673 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
674 }
675 }
676
677 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400678 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
679 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500680 */
681 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400682 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400683 if (mServiceComponent == null) {
684 // If we don't have a way to resume, just remove the player altogether
685 Log.d(TAG, "Removing unresumable controls");
686 removePlayer();
687 return;
688 }
689 resetButtons();
690 }
691
692 /**
693 * Hide the media buttons and show only a restart button
694 */
695 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500696 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700697
698 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
699 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
700 for (int i = 1; i < ACTION_IDS.length; i++) {
701 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
702 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500703 }
704
705 // Add a restart button
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700706 ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500707 btn.setOnClickListener(v -> {
708 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400709 if (mQSMediaBrowser != null) {
710 mQSMediaBrowser.disconnect();
711 }
712 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
713 @Override
714 public void onConnected() {
715 Log.d(TAG, "Successfully restarted");
716 }
717 @Override
718 public void onError() {
719 Log.e(TAG, "Error restarting");
720 mQSMediaBrowser.disconnect();
721 mQSMediaBrowser = null;
722 }
723 }, mServiceComponent);
724 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500725 });
726 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
727 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700728 expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
729 collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
730
731 mSeekBarViewModel.clearController();
732 // TODO: fix guts
733 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
734 View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
735
736 mMediaNotifView.setOnLongClickListener(v -> {
737 // Replace player view with close/cancel view
738// guts.setVisibility(View.GONE);
739 options.setVisibility(View.VISIBLE);
740 return true; // consumed click
741 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500742 }
743
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400744 private void makeActive() {
745 Assert.isMainThread();
746 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400747 if (mLocalMediaManager != null) {
748 mLocalMediaManager.registerCallback(mDeviceCallback);
749 mLocalMediaManager.startScan();
750 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400751 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500752 }
753 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400754
755 private void makeInactive() {
756 Assert.isMainThread();
757 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400758 if (mLocalMediaManager != null) {
759 mLocalMediaManager.stopScan();
760 mLocalMediaManager.unregisterCallback(mDeviceCallback);
761 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400762 mIsRegistered = false;
763 }
764 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400765 /**
766 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
767 * component to the list of resumption components
768 */
769 private void tryUpdateResumptionList(ComponentName componentName) {
770 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400771 if (mQSMediaBrowser != null) {
772 mQSMediaBrowser.disconnect();
773 }
774 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400775 new QSMediaBrowser.Callback() {
776 @Override
777 public void onConnected() {
778 Log.d(TAG, "yes we can resume with " + componentName);
779 mServiceComponent = componentName;
780 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400781 mQSMediaBrowser.disconnect();
782 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400783 }
784
785 @Override
786 public void onError() {
787 Log.d(TAG, "Cannot resume with " + componentName);
788 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400789 if (!hasMediaSession()) {
790 // If it's not active and we can't resume, remove
791 removePlayer();
792 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400793 mQSMediaBrowser.disconnect();
794 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400795 }
796 },
797 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400798 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400799 }
800
801 /**
802 * Add the component to the saved list of media browser services, checking for duplicates and
803 * removing older components that exceed the maximum limit
804 * @param componentName
805 */
806 private synchronized void updateResumptionList(ComponentName componentName) {
807 // Add to front of saved list
808 if (mSharedPrefs == null) {
809 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
810 }
811 String componentString = componentName.flattenToString();
812 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
813 if (listString == null) {
814 listString = componentString;
815 } else {
816 String[] components = listString.split(QSMediaBrowser.DELIMITER);
817 StringBuilder updated = new StringBuilder(componentString);
818 int nBrowsers = 1;
819 for (int i = 0; i < components.length
820 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
821 if (componentString.equals(components[i])) {
822 continue;
823 }
824 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
825 nBrowsers++;
826 }
827 listString = updated.toString();
828 }
829 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
830 }
831
832 /**
833 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
834 */
835 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500836}