blob: 6621ca13583c07cea01197ab0d7b04facc0110af [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.app.PendingIntent;
22import android.content.ComponentName;
23import 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;
Selim Cinekc5436712020-04-27 15:15:44 -070029import android.graphics.Bitmap;
30import android.graphics.Canvas;
31import android.graphics.Rect;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050032import android.graphics.drawable.Drawable;
33import android.graphics.drawable.GradientDrawable;
Selim Cinekc5436712020-04-27 15:15:44 -070034import android.graphics.drawable.Icon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050035import android.graphics.drawable.RippleDrawable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.media.session.MediaController;
Robert Snoebergerc981dc92020-04-27 15:00:50 -040037import android.media.session.MediaController.PlaybackInfo;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050038import android.media.session.MediaSession;
39import android.media.session.PlaybackState;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040040import android.service.media.MediaBrowserService;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050041import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050042import android.view.LayoutInflater;
43import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040044import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050045import android.view.ViewGroup;
46import android.widget.ImageButton;
47import android.widget.ImageView;
48import android.widget.LinearLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070049import android.widget.SeekBar;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050050import android.widget.TextView;
51
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040052import androidx.annotation.Nullable;
Selim Cinekf0f74952020-04-21 11:45:16 -070053import androidx.constraintlayout.motion.widget.Key;
54import androidx.constraintlayout.motion.widget.KeyAttributes;
55import androidx.constraintlayout.motion.widget.KeyFrames;
Selim Cinekd8357922020-04-10 15:06:53 -070056import androidx.constraintlayout.motion.widget.MotionLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070057import androidx.constraintlayout.widget.ConstraintSet;
Selim Cinekc5436712020-04-27 15:15:44 -070058import androidx.core.graphics.drawable.RoundedBitmapDrawable;
59import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050060
Selim Cinekc5436712020-04-27 15:15:44 -070061import com.android.settingslib.Utils;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040062import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050063import com.android.settingslib.media.MediaDevice;
64import com.android.settingslib.media.MediaOutputSliceConstants;
65import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050066import com.android.systemui.R;
67import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040068import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040069import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070070import com.android.systemui.util.concurrency.DelayableExecutor;
71
72import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050073
Selim Cinekf0f74952020-04-21 11:45:16 -070074import java.util.ArrayList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050075import java.util.List;
76import java.util.concurrent.Executor;
77
78/**
Selim Cinekd8357922020-04-10 15:06:53 -070079 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050080 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040081public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050082 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040083 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070084
85 // Button IDs for QS controls
86 static final int[] ACTION_IDS = {
87 R.id.action0,
88 R.id.action1,
89 R.id.action2,
90 R.id.action3,
91 R.id.action4
92 };
93
94 private final SeekBarViewModel mSeekBarViewModel;
95 private final SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040096 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040097 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040098 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050099
100 private Context mContext;
Selim Cinekf0f74952020-04-21 11:45:16 -0700101 private MotionLayout mMediaNotifView;
102 private final View mBackground;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500103 private View mSeamless;
104 private MediaSession.Token mToken;
105 private MediaController mController;
106 private int mForegroundColor;
107 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400108 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400109 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400110 private boolean mIsRegistered = false;
Selim Cinekf0f74952020-04-21 11:45:16 -0700111 private final List<KeyFrames> mKeyFrames;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400112 private String mKey;
Selim Cinekc5436712020-04-27 15:15:44 -0700113 private int mAlbumArtSize;
114 private int mAlbumArtRadius;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500115
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400116 public static final String MEDIA_PREFERENCES = "media_control_prefs";
117 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
118 private SharedPreferences mSharedPrefs;
119 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400120 private boolean mIsRemotePlayback;
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400121 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400122
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400123 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500124 @Override
125 public void onSessionDestroyed() {
126 Log.d(TAG, "session destroyed");
127 mController.unregisterCallback(mSessionCallback);
128 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400129 makeInactive();
130 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400131 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400132 public void onPlaybackStateChanged(PlaybackState state) {
133 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400134 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400135 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400136 clearControls();
137 makeInactive();
138 }
139 }
140 };
141
142 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
143 @Override
144 public void onViewAttachedToWindow(View unused) {
145 makeActive();
146 }
147 @Override
148 public void onViewDetachedFromWindow(View unused) {
149 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500150 }
151 };
152
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400153 private final LocalMediaManager.DeviceCallback mDeviceCallback =
154 new LocalMediaManager.DeviceCallback() {
155 @Override
156 public void onDeviceListUpdate(List<MediaDevice> devices) {
157 if (mLocalMediaManager == null) {
158 return;
159 }
160 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
161 // Check because this can be called several times while changing devices
162 if (mDevice == null || !mDevice.equals(currentDevice)) {
163 mDevice = currentDevice;
164 updateDevice(mDevice);
165 }
166 }
167
168 @Override
169 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
170 if (mDevice == null || !mDevice.equals(device)) {
171 mDevice = device;
172 updateDevice(mDevice);
173 }
174 }
175 };
176
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500177 /**
178 * Initialize a new control panel
179 * @param context
180 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400181 * @param routeManager Manager used to listen for device change events.
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400182 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500183 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400184 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500185 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400186 public MediaControlPanel(Context context, ViewGroup parent,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700187 @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
188 DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500189 mContext = context;
190 LayoutInflater inflater = LayoutInflater.from(mContext);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700191 mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
Selim Cinekf0f74952020-04-21 11:45:16 -0700192 mBackground = mMediaNotifView.findViewById(R.id.media_background);
193 mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400194 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
195 // mStateListener shouldn't need to be unregistered since this object shares the same
196 // lifecycle with the inflated view. It would be better, however, if this controller used an
197 // attach/detach of views instead of inflating them in the constructor, which would allow
198 // mStateListener to be unregistered in detach.
199 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400200 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400201 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500202 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400203 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700204 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
205 mSeekBarObserver = new SeekBarObserver(getView());
206 // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
207 // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
208 // will stop updating. So, use the lifecycle of the parent instead.
209 // TODO: this parent is also detached, need to fix that
210 mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
211 SeekBar bar = getView().findViewById(R.id.media_progress_bar);
212 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
213 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
Selim Cinekc5436712020-04-27 15:15:44 -0700214 loadDimens();
215 }
216
217 private void loadDimens() {
218 mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
219 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
220 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500221 }
222
223 /**
224 * Get the view used to display media controls
225 * @return the view
226 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700227 public MotionLayout getView() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500228 return mMediaNotifView;
229 }
230
231 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700232 * Sets the listening state of the player.
233 *
234 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
235 * unnecessary work when the QS panel is closed.
236 *
237 * @param listening True when player should be active. Otherwise, false.
238 */
239 public void setListening(boolean listening) {
240 mSeekBarViewModel.setListening(listening);
241 }
242
243 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500244 * Get the context
245 * @return context
246 */
247 public Context getContext() {
248 return mContext;
249 }
250
251 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700252 * Bind this view based on the data given
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500253 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700254 public void bind(@NotNull MediaData data) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700255 MediaSession.Token token = data.getToken();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700256 mForegroundColor = data.getForegroundColor();
257 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400258 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400259 if (mQSMediaBrowser != null) {
260 Log.d(TAG, "Disconnecting old media browser");
261 mQSMediaBrowser.disconnect();
262 mQSMediaBrowser = null;
263 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400264 mToken = token;
265 mServiceComponent = null;
266 mCheckedForResumption = false;
267 }
268
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500269 mController = new MediaController(mContext, mToken);
270
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400271 // Try to find a browser service component for this app
272 // TODO also check for a media button receiver intended for restarting (b/154127084)
273 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400274 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400275 if (mServiceComponent == null && !mCheckedForResumption) {
276 Log.d(TAG, "Checking for service component");
277 PackageManager pm = mContext.getPackageManager();
278 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
279 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700280 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400281 if (resumeInfo != null) {
282 for (ResolveInfo inf : resumeInfo) {
283 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
284 mBackgroundExecutor.execute(() ->
285 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
286 break;
287 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500288 }
289 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400290 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500291 }
292
293 mController.registerCallback(mSessionCallback);
294
Selim Cinekf0f74952020-04-21 11:45:16 -0700295 mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
296 ColorStateList.valueOf(mBackgroundColor));
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500297
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
Selim Cinekf0f74952020-04-21 11:45:16 -0700306 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
Selim Cinekc5436712020-04-27 15:15:44 -0700307 // TODO: migrate this to a view with rounded corners instead of baking the rounding
308 // into the bitmap
309 Drawable artwork = createRoundedBitmap(data.getArtwork());
310 albumView.setImageDrawable(artwork);
Selim Cinekf0f74952020-04-21 11:45:16 -0700311
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500312 // App icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700313 ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
314 // TODO: look at iconDrawable
315 Drawable iconDrawable = data.getAppIcon();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500316 iconDrawable.setTint(mForegroundColor);
317 appIcon.setImageDrawable(iconDrawable);
318
Selim Cinekf0f74952020-04-21 11:45:16 -0700319 // Song name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700320 TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
321 titleText.setText(data.getSong());
Selim Cinekf0f74952020-04-21 11:45:16 -0700322 titleText.setTextColor(data.getForegroundColor());
323
324 // App title
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700325 TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
326 appName.setText(data.getApp());
327 appName.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700328
329 // Artist name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700330 TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
331 artistText.setText(data.getArtist());
332 artistText.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700333
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500334 // Transfer chip
335 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400336 if (mSeamless != null) {
337 if (mLocalMediaManager != null) {
338 mSeamless.setVisibility(View.VISIBLE);
339 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
340 mSeamless.setOnClickListener(v -> {
341 final Intent intent = new Intent()
342 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
343 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
344 mController.getPackageName())
345 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
346 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
347 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
348 });
349 } else {
350 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
351 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500352 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400353 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
354 if (playbackInfo != null) {
355 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
356 } else {
357 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
358 mIsRemotePlayback = false;
359 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500360
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700361 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
362 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
363 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
364 // Media controls
365 int i = 0;
366 List<MediaAction> actionIcons = data.getActions();
367 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700368 int actionId = ACTION_IDS[i];
369 final ImageButton button = mMediaNotifView.findViewById(actionId);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700370 MediaAction mediaAction = actionIcons.get(i);
371 button.setImageDrawable(mediaAction.getDrawable());
372 button.setContentDescription(mediaAction.getContentDescription());
373 button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
374 PendingIntent actionIntent = mediaAction.getIntent();
375
Selim Cinekf0f74952020-04-21 11:45:16 -0700376 if (mBackground.getBackground() instanceof IlluminationDrawable) {
377 ((IlluminationDrawable) mBackground.getBackground())
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700378 .setupTouch(button, mMediaNotifView);
379 }
380
381 button.setOnClickListener(v -> {
382 if (actionIntent != null) {
383 try {
384 actionIntent.send();
385 } catch (PendingIntent.CanceledException e) {
386 e.printStackTrace();
387 }
388 }
389 });
390 boolean visibleInCompat = actionsWhenCollapsed.contains(i);
Selim Cinekf0f74952020-04-21 11:45:16 -0700391 updateKeyFrameVisibility(actionId, visibleInCompat);
392 collapsedSet.setVisibility(actionId,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700393 visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
Selim Cinekf0f74952020-04-21 11:45:16 -0700394 expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700395 }
396
397 // Hide any unused buttons
398 for (; i < ACTION_IDS.length; i++) {
399 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
400 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
401 }
402
403 // Seek Bar
404 final MediaController controller = new MediaController(getContext(), data.getToken());
405 mBackgroundExecutor.execute(
406 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
407
408 // Set up long press menu
409 // TODO: b/156036025 bring back media guts
410
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400411 makeActive();
Selim Cinekf0f74952020-04-21 11:45:16 -0700412 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400413
Selim Cinekc5436712020-04-27 15:15:44 -0700414 private Drawable createRoundedBitmap(Icon icon) {
415 if (icon == null) {
416 return null;
417 }
418 // Let's scale down the View, such that the content always nicely fills the view.
419 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
420 // ratios
421 Drawable drawable = icon.loadDrawable(mContext);
422 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
423 Rect bounds;
424 if (aspectRatio > 1.0f) {
425 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
426 } else {
427 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
428 }
429 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
430 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
431 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
432 bounds.offset((int) -offsetX,(int) -offsetY);
433 }
434 drawable.setBounds(bounds);
435 Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize,
436 Bitmap.Config.ARGB_8888);
437 Canvas canvas = new Canvas(scaled);
438 drawable.draw(canvas);
439 RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create(
440 mContext.getResources(), scaled);
441 artwork.setCornerRadius(mAlbumArtRadius);
442 return artwork;
443 }
444
Selim Cinekf0f74952020-04-21 11:45:16 -0700445 /**
446 * Updates the keyframe visibility such that only views that are not visible actually go
447 * through a transition and fade in.
448 *
449 * @param actionId the id to change
450 * @param visible is the view visible
451 */
452 private void updateKeyFrameVisibility(int actionId, boolean visible) {
453 for (int i = 0; i < mKeyFrames.size(); i++) {
454 KeyFrames keyframe = mKeyFrames.get(i);
455 ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
456 for (int j = 0; j < viewKeyFrames.size(); j++) {
457 Key key = viewKeyFrames.get(j);
458 if (key instanceof KeyAttributes) {
459 KeyAttributes attributes = (KeyAttributes) key;
460 attributes.setValue("alpha", visible ? 1.0f : 0.0f);
461 }
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400462 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400463 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500464 }
465
466 /**
467 * Return the token for the current media session
468 * @return the token
469 */
470 public MediaSession.Token getMediaSessionToken() {
471 return mToken;
472 }
473
474 /**
475 * Get the current media controller
476 * @return the controller
477 */
478 public MediaController getController() {
479 return mController;
480 }
481
482 /**
483 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400484 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500485 */
486 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400487 if (mController == null) {
488 return null;
489 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500490 return mController.getPackageName();
491 }
492
493 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400494 * Return the original notification's key
495 * @return The notification key
496 */
497 public String getKey() {
498 return mKey;
499 }
500
501 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500502 * Check whether this player has an attached media session.
503 * @return whether there is a controller with a current media session.
504 */
505 public boolean hasMediaSession() {
506 return mController != null && mController.getPlaybackState() != null;
507 }
508
509 /**
510 * Check whether the media controlled by this player is currently playing
511 * @return whether it is playing, or false if no controller information
512 */
513 public boolean isPlaying() {
514 return isPlaying(mController);
515 }
516
517 /**
518 * Check whether the given controller is currently playing
519 * @param controller media controller to check
520 * @return whether it is playing, or false if no controller information
521 */
522 protected boolean isPlaying(MediaController controller) {
523 if (controller == null) {
524 return false;
525 }
526
527 PlaybackState state = controller.getPlaybackState();
528 if (state == null) {
529 return false;
530 }
531
532 return (state.getState() == PlaybackState.STATE_PLAYING);
533 }
534
535 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500536 * Update the current device information
537 * @param device device information to display
538 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400539 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500540 if (mSeamless == null) {
541 return;
542 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400543 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500544 updateChipInternal(device);
545 });
546 }
547
548 private void updateChipInternal(MediaDevice device) {
549 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
550
551 // Update the outline color
552 LinearLayout viewLayout = (LinearLayout) mSeamless;
553 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
554 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
555 rect.setStroke(2, mForegroundColor);
556 rect.setColor(mBackgroundColor);
557
558 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
559 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
560 deviceName.setTextColor(fgTintList);
561
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400562 if (mIsRemotePlayback) {
563 mSeamless.setEnabled(false);
564 mSeamless.setAlpha(0.38f);
565 iconView.setImageResource(R.drawable.ic_hardware_speaker);
566 iconView.setVisibility(View.VISIBLE);
567 iconView.setImageTintList(fgTintList);
568 deviceName.setText(R.string.media_seamless_remote_device);
569 } else if (device != null) {
570 mSeamless.setEnabled(true);
571 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500572 Drawable icon = device.getIcon();
573 iconView.setVisibility(View.VISIBLE);
574 iconView.setImageTintList(fgTintList);
575
576 if (icon instanceof AdaptiveIcon) {
577 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
578 aIcon.setBackgroundColor(mBackgroundColor);
579 iconView.setImageDrawable(aIcon);
580 } else {
581 iconView.setImageDrawable(icon);
582 }
583 deviceName.setText(device.getName());
584 } else {
585 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400586 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400587 mSeamless.setEnabled(true);
588 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500589 iconView.setVisibility(View.GONE);
590 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
591 }
592 }
593
594 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400595 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
596 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500597 */
598 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400599 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400600 if (mServiceComponent == null) {
601 // If we don't have a way to resume, just remove the player altogether
602 Log.d(TAG, "Removing unresumable controls");
603 removePlayer();
604 return;
605 }
606 resetButtons();
607 }
608
609 /**
610 * Hide the media buttons and show only a restart button
611 */
612 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500613 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700614
615 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
616 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
617 for (int i = 1; i < ACTION_IDS.length; i++) {
618 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
619 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500620 }
621
622 // Add a restart button
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700623 ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500624 btn.setOnClickListener(v -> {
625 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400626 if (mQSMediaBrowser != null) {
627 mQSMediaBrowser.disconnect();
628 }
629 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
630 @Override
631 public void onConnected() {
632 Log.d(TAG, "Successfully restarted");
633 }
634 @Override
635 public void onError() {
636 Log.e(TAG, "Error restarting");
637 mQSMediaBrowser.disconnect();
638 mQSMediaBrowser = null;
639 }
640 }, mServiceComponent);
641 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500642 });
643 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
644 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700645 expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
646 collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
647
648 mSeekBarViewModel.clearController();
649 // TODO: fix guts
650 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
651 View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
652
653 mMediaNotifView.setOnLongClickListener(v -> {
654 // Replace player view with close/cancel view
655// guts.setVisibility(View.GONE);
656 options.setVisibility(View.VISIBLE);
657 return true; // consumed click
658 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500659 }
660
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400661 private void makeActive() {
662 Assert.isMainThread();
663 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400664 if (mLocalMediaManager != null) {
665 mLocalMediaManager.registerCallback(mDeviceCallback);
666 mLocalMediaManager.startScan();
667 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400668 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500669 }
670 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400671
672 private void makeInactive() {
673 Assert.isMainThread();
674 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400675 if (mLocalMediaManager != null) {
676 mLocalMediaManager.stopScan();
677 mLocalMediaManager.unregisterCallback(mDeviceCallback);
678 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400679 mIsRegistered = false;
680 }
681 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400682 /**
683 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
684 * component to the list of resumption components
685 */
686 private void tryUpdateResumptionList(ComponentName componentName) {
687 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400688 if (mQSMediaBrowser != null) {
689 mQSMediaBrowser.disconnect();
690 }
691 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400692 new QSMediaBrowser.Callback() {
693 @Override
694 public void onConnected() {
695 Log.d(TAG, "yes we can resume with " + componentName);
696 mServiceComponent = componentName;
697 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400698 mQSMediaBrowser.disconnect();
699 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400700 }
701
702 @Override
703 public void onError() {
704 Log.d(TAG, "Cannot resume with " + componentName);
705 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400706 if (!hasMediaSession()) {
707 // If it's not active and we can't resume, remove
708 removePlayer();
709 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400710 mQSMediaBrowser.disconnect();
711 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400712 }
713 },
714 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400715 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400716 }
717
718 /**
719 * Add the component to the saved list of media browser services, checking for duplicates and
720 * removing older components that exceed the maximum limit
721 * @param componentName
722 */
723 private synchronized void updateResumptionList(ComponentName componentName) {
724 // Add to front of saved list
725 if (mSharedPrefs == null) {
726 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
727 }
728 String componentString = componentName.flattenToString();
729 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
730 if (listString == null) {
731 listString = componentString;
732 } else {
733 String[] components = listString.split(QSMediaBrowser.DELIMITER);
734 StringBuilder updated = new StringBuilder(componentString);
735 int nBrowsers = 1;
736 for (int i = 0; i < components.length
737 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
738 if (componentString.equals(components[i])) {
739 continue;
740 }
741 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
742 nBrowsers++;
743 }
744 listString = updated.toString();
745 }
746 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
747 }
748
749 /**
750 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
751 */
752 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500753}