blob: c3a7d9fbdd50a73ea1b4e8dcdf784385b6b53b12 [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
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050019import android.app.PendingIntent;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040023import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050024import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.content.res.ColorStateList;
Selim Cinekc5436712020-04-27 15:15:44 -070027import android.graphics.Bitmap;
28import android.graphics.Canvas;
29import android.graphics.Rect;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050030import android.graphics.drawable.Drawable;
31import android.graphics.drawable.GradientDrawable;
Selim Cinekc5436712020-04-27 15:15:44 -070032import android.graphics.drawable.Icon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050033import android.graphics.drawable.RippleDrawable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050034import android.media.session.MediaController;
Robert Snoebergerc981dc92020-04-27 15:00:50 -040035import android.media.session.MediaController.PlaybackInfo;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.media.session.MediaSession;
37import android.media.session.PlaybackState;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040038import android.service.media.MediaBrowserService;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050039import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050040import android.view.View;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050041import android.widget.ImageButton;
42import android.widget.ImageView;
43import android.widget.LinearLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070044import android.widget.SeekBar;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050045import android.widget.TextView;
46
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040047import androidx.annotation.Nullable;
Selim Cinekf418bb02020-05-04 17:16:58 -070048import androidx.annotation.UiThread;
Selim Cinekf0f74952020-04-21 11:45:16 -070049import androidx.constraintlayout.motion.widget.Key;
50import androidx.constraintlayout.motion.widget.KeyAttributes;
51import androidx.constraintlayout.motion.widget.KeyFrames;
Selim Cinekd8357922020-04-10 15:06:53 -070052import androidx.constraintlayout.motion.widget.MotionLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070053import androidx.constraintlayout.widget.ConstraintSet;
Selim Cinekc5436712020-04-27 15:15:44 -070054import androidx.core.graphics.drawable.RoundedBitmapDrawable;
55import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050056
Selim Cinekc5436712020-04-27 15:15:44 -070057import com.android.settingslib.Utils;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040058import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050059import com.android.settingslib.media.MediaDevice;
60import com.android.settingslib.media.MediaOutputSliceConstants;
61import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050062import com.android.systemui.R;
63import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040064import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040065import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070066import com.android.systemui.util.concurrency.DelayableExecutor;
67
68import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050069
Selim Cinekf0f74952020-04-21 11:45:16 -070070import java.util.ArrayList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050071import java.util.List;
72import java.util.concurrent.Executor;
73
74/**
Selim Cinekd8357922020-04-10 15:06:53 -070075 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050076 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040077public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050078 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040079 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070080
81 // Button IDs for QS controls
82 static final int[] ACTION_IDS = {
83 R.id.action0,
84 R.id.action1,
85 R.id.action2,
86 R.id.action3,
87 R.id.action4
88 };
89
90 private final SeekBarViewModel mSeekBarViewModel;
Robert Snoebergereb49e942020-05-12 16:31:09 -040091 private SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040092 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040093 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040094 private final ActivityStarter mActivityStarter;
Robert Snoebergereb49e942020-05-12 16:31:09 -040095 private LayoutAnimationHelper mLayoutAnimationHelper;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050096
97 private Context mContext;
Robert Snoebergereb49e942020-05-12 16:31:09 -040098 private PlayerViewHolder mViewHolder;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050099 private MediaSession.Token mToken;
100 private MediaController mController;
101 private int mForegroundColor;
102 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400103 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400104 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400105 private boolean mIsRegistered = false;
Robert Snoebergereb49e942020-05-12 16:31:09 -0400106 private List<KeyFrames> mKeyFrames;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400107 private String mKey;
Selim Cinekc5436712020-04-27 15:15:44 -0700108 private int mAlbumArtSize;
109 private int mAlbumArtRadius;
Selim Cinek3df592e2020-04-28 13:51:43 -0700110 private int mViewWidth;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500111
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400112 public static final String MEDIA_PREFERENCES = "media_control_prefs";
113 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
114 private SharedPreferences mSharedPrefs;
115 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400116 private boolean mIsRemotePlayback;
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400117 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400118
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400119 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500120 @Override
121 public void onSessionDestroyed() {
122 Log.d(TAG, "session destroyed");
123 mController.unregisterCallback(mSessionCallback);
124 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400125 makeInactive();
126 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400127 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400128 public void onPlaybackStateChanged(PlaybackState state) {
129 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400130 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400131 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400132 clearControls();
133 makeInactive();
134 }
135 }
136 };
137
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400138 private final LocalMediaManager.DeviceCallback mDeviceCallback =
139 new LocalMediaManager.DeviceCallback() {
140 @Override
141 public void onDeviceListUpdate(List<MediaDevice> devices) {
142 if (mLocalMediaManager == null) {
143 return;
144 }
145 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
146 // Check because this can be called several times while changing devices
147 if (mDevice == null || !mDevice.equals(currentDevice)) {
148 mDevice = currentDevice;
149 updateDevice(mDevice);
150 }
151 }
152
153 @Override
154 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
155 if (mDevice == null || !mDevice.equals(device)) {
156 mDevice = device;
157 updateDevice(mDevice);
158 }
159 }
160 };
161
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500162 /**
163 * Initialize a new control panel
164 * @param context
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400165 * @param routeManager Manager used to listen for device change events.
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400166 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500167 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400168 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500169 */
Robert Snoebergereb49e942020-05-12 16:31:09 -0400170 public MediaControlPanel(Context context, @Nullable LocalMediaManager routeManager,
171 Executor foregroundExecutor, DelayableExecutor backgroundExecutor,
172 ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500173 mContext = context;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400174 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400175 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500176 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400177 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700178 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
Selim Cinekc5436712020-04-27 15:15:44 -0700179 loadDimens();
180 }
181
Selim Cinek098baf42020-04-27 19:02:06 -0700182 public void onDestroy() {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400183 if (mSeekBarObserver != null) {
184 mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
185 }
Selim Cinek098baf42020-04-27 19:02:06 -0700186 makeInactive();
187 }
188
Selim Cinekc5436712020-04-27 15:15:44 -0700189 private void loadDimens() {
190 mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
191 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
192 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500193 }
194
195 /**
Robert Snoebergereb49e942020-05-12 16:31:09 -0400196 * Get the view holder used to display media controls
197 * @return the view holder
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500198 */
Robert Snoebergereb49e942020-05-12 16:31:09 -0400199 @Nullable
200 public PlayerViewHolder getView() {
201 return mViewHolder;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500202 }
203
204 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700205 * Sets the listening state of the player.
206 *
207 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
208 * unnecessary work when the QS panel is closed.
209 *
210 * @param listening True when player should be active. Otherwise, false.
211 */
212 public void setListening(boolean listening) {
213 mSeekBarViewModel.setListening(listening);
214 }
215
216 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500217 * Get the context
218 * @return context
219 */
220 public Context getContext() {
221 return mContext;
222 }
223
Robert Snoebergereb49e942020-05-12 16:31:09 -0400224 /** Attaches the player to the view holder. */
225 public void attach(PlayerViewHolder vh) {
226 mViewHolder = vh;
227 MotionLayout motionView = vh.getPlayer();
228 mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
229 GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
230 mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
231 mSeekBarObserver = new SeekBarObserver(motionView);
232 mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
233 SeekBar bar = vh.getSeekBar();
234 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
235 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
236 }
237
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500238 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700239 * Bind this view based on the data given
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500240 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700241 public void bind(@NotNull MediaData data) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400242 if (mViewHolder == null) {
243 return;
244 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700245 MediaSession.Token token = data.getToken();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700246 mForegroundColor = data.getForegroundColor();
247 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400248 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400249 if (mQSMediaBrowser != null) {
250 Log.d(TAG, "Disconnecting old media browser");
251 mQSMediaBrowser.disconnect();
252 mQSMediaBrowser = null;
253 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400254 mToken = token;
255 mServiceComponent = null;
256 mCheckedForResumption = false;
257 }
258
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500259 mController = new MediaController(mContext, mToken);
260
Robert Snoebergereb49e942020-05-12 16:31:09 -0400261 ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
262 ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
Selim Cinek8081f092020-05-01 21:11:13 -0700263
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400264 // Try to find a browser service component for this app
265 // TODO also check for a media button receiver intended for restarting (b/154127084)
266 // Only check if we haven't tried yet or the session token changed
Selim Cinekf418bb02020-05-04 17:16:58 -0700267 final String pkgName = data.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400268 if (mServiceComponent == null && !mCheckedForResumption) {
269 Log.d(TAG, "Checking for service component");
270 PackageManager pm = mContext.getPackageManager();
271 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
272 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700273 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400274 if (resumeInfo != null) {
275 for (ResolveInfo inf : resumeInfo) {
276 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
277 mBackgroundExecutor.execute(() ->
278 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
279 break;
280 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500281 }
282 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400283 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500284 }
285
286 mController.registerCallback(mSessionCallback);
287
Robert Snoebergereb49e942020-05-12 16:31:09 -0400288 mViewHolder.getBackground().setBackgroundTintList(
Selim Cinekf0f74952020-04-21 11:45:16 -0700289 ColorStateList.valueOf(mBackgroundColor));
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500290
291 // Click action
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700292 PendingIntent clickIntent = data.getClickIntent();
293 if (clickIntent != null) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400294 mViewHolder.getPlayer().setOnClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700295 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400296 });
297 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500298
Robert Snoebergereb49e942020-05-12 16:31:09 -0400299 ImageView albumView = mViewHolder.getAlbumView();
Selim Cinekc5436712020-04-27 15:15:44 -0700300 // TODO: migrate this to a view with rounded corners instead of baking the rounding
301 // into the bitmap
302 Drawable artwork = createRoundedBitmap(data.getArtwork());
303 albumView.setImageDrawable(artwork);
Selim Cinekf0f74952020-04-21 11:45:16 -0700304
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500305 // App icon
Robert Snoebergereb49e942020-05-12 16:31:09 -0400306 ImageView appIcon = mViewHolder.getAppIcon();
Selim Cinekf418bb02020-05-04 17:16:58 -0700307 Drawable iconDrawable = data.getAppIcon().mutate();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500308 iconDrawable.setTint(mForegroundColor);
309 appIcon.setImageDrawable(iconDrawable);
310
Selim Cinekf0f74952020-04-21 11:45:16 -0700311 // Song name
Robert Snoebergereb49e942020-05-12 16:31:09 -0400312 TextView titleText = mViewHolder.getTitleText();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700313 titleText.setText(data.getSong());
Selim Cinekf0f74952020-04-21 11:45:16 -0700314 titleText.setTextColor(data.getForegroundColor());
315
316 // App title
Robert Snoebergereb49e942020-05-12 16:31:09 -0400317 TextView appName = mViewHolder.getAppName();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700318 appName.setText(data.getApp());
319 appName.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700320
321 // Artist name
Robert Snoebergereb49e942020-05-12 16:31:09 -0400322 TextView artistText = mViewHolder.getArtistText();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700323 artistText.setText(data.getArtist());
324 artistText.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700325
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500326 // Transfer chip
Robert Snoebergereb49e942020-05-12 16:31:09 -0400327 if (mLocalMediaManager != null) {
328 mViewHolder.getSeamless().setVisibility(View.VISIBLE);
329 setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
330 setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
331 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
332 mViewHolder.getSeamless().setOnClickListener(v -> {
333 final Intent intent = new Intent()
334 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
335 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
336 mController.getPackageName())
337 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
338 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
339 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
340 });
341 } else {
342 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500343 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400344 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
345 if (playbackInfo != null) {
346 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
347 } else {
348 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
349 mIsRemotePlayback = false;
350 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700351 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
352 // Media controls
353 int i = 0;
354 List<MediaAction> actionIcons = data.getActions();
355 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700356 int actionId = ACTION_IDS[i];
Robert Snoebergereb49e942020-05-12 16:31:09 -0400357 final ImageButton button = mViewHolder.getAction(actionId);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700358 MediaAction mediaAction = actionIcons.get(i);
359 button.setImageDrawable(mediaAction.getDrawable());
360 button.setContentDescription(mediaAction.getContentDescription());
361 button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
362 PendingIntent actionIntent = mediaAction.getIntent();
363
Robert Snoebergereb49e942020-05-12 16:31:09 -0400364 if (mViewHolder.getBackground().getBackground() instanceof IlluminationDrawable) {
365 ((IlluminationDrawable) mViewHolder.getBackground().getBackground())
366 .setupTouch(button, mViewHolder.getPlayer());
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700367 }
368
369 button.setOnClickListener(v -> {
370 if (actionIntent != null) {
371 try {
372 actionIntent.send();
373 } catch (PendingIntent.CanceledException e) {
374 e.printStackTrace();
375 }
376 }
377 });
378 boolean visibleInCompat = actionsWhenCollapsed.contains(i);
Selim Cinekf0f74952020-04-21 11:45:16 -0700379 updateKeyFrameVisibility(actionId, visibleInCompat);
Selim Cinek8081f092020-05-01 21:11:13 -0700380 setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
381 setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700382 }
383
384 // Hide any unused buttons
385 for (; i < ACTION_IDS.length; i++) {
Selim Cinek8081f092020-05-01 21:11:13 -0700386 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
387 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700388 }
389
390 // Seek Bar
Selim Cinekf418bb02020-05-04 17:16:58 -0700391 final MediaController controller = getController();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700392 mBackgroundExecutor.execute(
393 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
394
395 // Set up long press menu
396 // TODO: b/156036025 bring back media guts
397
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400398 makeActive();
Selim Cinekf418bb02020-05-04 17:16:58 -0700399
400 // Update both constraint sets to regenerate the animation.
Robert Snoebergereb49e942020-05-12 16:31:09 -0400401 mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
402 mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
Selim Cinekf0f74952020-04-21 11:45:16 -0700403 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400404
Selim Cinekf418bb02020-05-04 17:16:58 -0700405 @UiThread
Selim Cinekc5436712020-04-27 15:15:44 -0700406 private Drawable createRoundedBitmap(Icon icon) {
407 if (icon == null) {
408 return null;
409 }
410 // Let's scale down the View, such that the content always nicely fills the view.
411 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
412 // ratios
413 Drawable drawable = icon.loadDrawable(mContext);
414 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
415 Rect bounds;
416 if (aspectRatio > 1.0f) {
417 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
418 } else {
419 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
420 }
421 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
422 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
423 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
424 bounds.offset((int) -offsetX,(int) -offsetY);
425 }
426 drawable.setBounds(bounds);
427 Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize,
428 Bitmap.Config.ARGB_8888);
429 Canvas canvas = new Canvas(scaled);
430 drawable.draw(canvas);
431 RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create(
432 mContext.getResources(), scaled);
433 artwork.setCornerRadius(mAlbumArtRadius);
434 return artwork;
435 }
436
Selim Cinekf0f74952020-04-21 11:45:16 -0700437 /**
438 * Updates the keyframe visibility such that only views that are not visible actually go
439 * through a transition and fade in.
440 *
441 * @param actionId the id to change
442 * @param visible is the view visible
443 */
444 private void updateKeyFrameVisibility(int actionId, boolean visible) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400445 if (mKeyFrames == null) {
446 return;
447 }
Selim Cinekf0f74952020-04-21 11:45:16 -0700448 for (int i = 0; i < mKeyFrames.size(); i++) {
449 KeyFrames keyframe = mKeyFrames.get(i);
450 ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
451 for (int j = 0; j < viewKeyFrames.size(); j++) {
452 Key key = viewKeyFrames.get(j);
453 if (key instanceof KeyAttributes) {
454 KeyAttributes attributes = (KeyAttributes) key;
455 attributes.setValue("alpha", visible ? 1.0f : 0.0f);
456 }
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400457 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400458 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500459 }
460
461 /**
462 * Return the token for the current media session
463 * @return the token
464 */
465 public MediaSession.Token getMediaSessionToken() {
466 return mToken;
467 }
468
469 /**
470 * Get the current media controller
471 * @return the controller
472 */
473 public MediaController getController() {
474 return mController;
475 }
476
477 /**
478 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400479 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500480 */
481 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400482 if (mController == null) {
483 return null;
484 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500485 return mController.getPackageName();
486 }
487
488 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400489 * Return the original notification's key
490 * @return The notification key
491 */
492 public String getKey() {
493 return mKey;
494 }
495
496 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500497 * Check whether this player has an attached media session.
498 * @return whether there is a controller with a current media session.
499 */
500 public boolean hasMediaSession() {
501 return mController != null && mController.getPlaybackState() != null;
502 }
503
504 /**
505 * Check whether the media controlled by this player is currently playing
506 * @return whether it is playing, or false if no controller information
507 */
508 public boolean isPlaying() {
509 return isPlaying(mController);
510 }
511
512 /**
513 * Check whether the given controller is currently playing
514 * @param controller media controller to check
515 * @return whether it is playing, or false if no controller information
516 */
517 protected boolean isPlaying(MediaController controller) {
518 if (controller == null) {
519 return false;
520 }
521
522 PlaybackState state = controller.getPlaybackState();
523 if (state == null) {
524 return false;
525 }
526
527 return (state.getState() == PlaybackState.STATE_PLAYING);
528 }
529
530 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500531 * Update the current device information
532 * @param device device information to display
533 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400534 private void updateDevice(MediaDevice device) {
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400535 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500536 updateChipInternal(device);
537 });
538 }
539
540 private void updateChipInternal(MediaDevice device) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400541 if (mViewHolder == null) {
542 return;
543 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500544 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
545
546 // Update the outline color
Robert Snoebergereb49e942020-05-12 16:31:09 -0400547 LinearLayout viewLayout = (LinearLayout) mViewHolder.getSeamless();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500548 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
549 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
550 rect.setStroke(2, mForegroundColor);
551 rect.setColor(mBackgroundColor);
552
Robert Snoebergereb49e942020-05-12 16:31:09 -0400553 ImageView iconView = mViewHolder.getSeamlessIcon();
554 TextView deviceName = mViewHolder.getSeamlessText();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500555 deviceName.setTextColor(fgTintList);
556
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400557 if (mIsRemotePlayback) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400558 mViewHolder.getSeamless().setEnabled(false);
559 mViewHolder.getSeamless().setAlpha(0.38f);
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400560 iconView.setImageResource(R.drawable.ic_hardware_speaker);
561 iconView.setVisibility(View.VISIBLE);
562 iconView.setImageTintList(fgTintList);
563 deviceName.setText(R.string.media_seamless_remote_device);
564 } else if (device != null) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400565 mViewHolder.getSeamless().setEnabled(true);
566 mViewHolder.getSeamless().setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500567 Drawable icon = device.getIcon();
568 iconView.setVisibility(View.VISIBLE);
569 iconView.setImageTintList(fgTintList);
570
571 if (icon instanceof AdaptiveIcon) {
572 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
573 aIcon.setBackgroundColor(mBackgroundColor);
574 iconView.setImageDrawable(aIcon);
575 } else {
576 iconView.setImageDrawable(icon);
577 }
578 deviceName.setText(device.getName());
579 } else {
580 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400581 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergereb49e942020-05-12 16:31:09 -0400582 mViewHolder.getSeamless().setEnabled(true);
583 mViewHolder.getSeamless().setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500584 iconView.setVisibility(View.GONE);
585 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
586 }
587 }
588
589 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400590 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
591 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500592 */
593 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400594 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400595 if (mServiceComponent == null) {
596 // If we don't have a way to resume, just remove the player altogether
597 Log.d(TAG, "Removing unresumable controls");
598 removePlayer();
599 return;
600 }
601 resetButtons();
602 }
603
604 /**
605 * Hide the media buttons and show only a restart button
606 */
607 protected void resetButtons() {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400608 if (mViewHolder == null) {
609 return;
610 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500611 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700612
Robert Snoebergereb49e942020-05-12 16:31:09 -0400613 ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
614 ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700615 for (int i = 1; i < ACTION_IDS.length; i++) {
Selim Cinek8081f092020-05-01 21:11:13 -0700616 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
617 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500618 }
619
620 // Add a restart button
Robert Snoebergereb49e942020-05-12 16:31:09 -0400621 ImageButton btn = mViewHolder.getAction0();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500622 btn.setOnClickListener(v -> {
623 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400624 if (mQSMediaBrowser != null) {
625 mQSMediaBrowser.disconnect();
626 }
627 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
628 @Override
629 public void onConnected() {
630 Log.d(TAG, "Successfully restarted");
631 }
632 @Override
633 public void onError() {
634 Log.e(TAG, "Error restarting");
635 mQSMediaBrowser.disconnect();
636 mQSMediaBrowser = null;
637 }
638 }, mServiceComponent);
639 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500640 });
641 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
642 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek8081f092020-05-01 21:11:13 -0700643 setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
644 setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700645
646 mSeekBarViewModel.clearController();
647 // TODO: fix guts
648 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
Robert Snoebergereb49e942020-05-12 16:31:09 -0400649 View options = mViewHolder.getOptions();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700650
Robert Snoebergereb49e942020-05-12 16:31:09 -0400651 mViewHolder.getPlayer().setOnLongClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700652 // Replace player view with close/cancel view
653// guts.setVisibility(View.GONE);
654 options.setVisibility(View.VISIBLE);
655 return true; // consumed click
656 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500657 }
658
Selim Cinek8081f092020-05-01 21:11:13 -0700659 private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
660 set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
661 set.setAlpha(actionId, visible ? 1.0f : 0.0f);
662 }
663
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400664 private void makeActive() {
665 Assert.isMainThread();
666 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400667 if (mLocalMediaManager != null) {
668 mLocalMediaManager.registerCallback(mDeviceCallback);
669 mLocalMediaManager.startScan();
670 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400671 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500672 }
673 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400674
675 private void makeInactive() {
676 Assert.isMainThread();
677 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400678 if (mLocalMediaManager != null) {
679 mLocalMediaManager.stopScan();
680 mLocalMediaManager.unregisterCallback(mDeviceCallback);
681 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400682 mIsRegistered = false;
683 }
684 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400685 /**
686 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
687 * component to the list of resumption components
688 */
689 private void tryUpdateResumptionList(ComponentName componentName) {
690 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400691 if (mQSMediaBrowser != null) {
692 mQSMediaBrowser.disconnect();
693 }
694 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400695 new QSMediaBrowser.Callback() {
696 @Override
697 public void onConnected() {
698 Log.d(TAG, "yes we can resume with " + componentName);
699 mServiceComponent = componentName;
700 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400701 mQSMediaBrowser.disconnect();
702 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400703 }
704
705 @Override
706 public void onError() {
707 Log.d(TAG, "Cannot resume with " + componentName);
708 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400709 if (!hasMediaSession()) {
710 // If it's not active and we can't resume, remove
711 removePlayer();
712 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400713 mQSMediaBrowser.disconnect();
714 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400715 }
716 },
717 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400718 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400719 }
720
721 /**
722 * Add the component to the saved list of media browser services, checking for duplicates and
723 * removing older components that exceed the maximum limit
724 * @param componentName
725 */
726 private synchronized void updateResumptionList(ComponentName componentName) {
727 // Add to front of saved list
728 if (mSharedPrefs == null) {
729 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
730 }
731 String componentString = componentName.flattenToString();
732 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
733 if (listString == null) {
734 listString = componentString;
735 } else {
736 String[] components = listString.split(QSMediaBrowser.DELIMITER);
737 StringBuilder updated = new StringBuilder(componentString);
738 int nBrowsers = 1;
739 for (int i = 0; i < components.length
740 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
741 if (componentString.equals(components[i])) {
742 continue;
743 }
744 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
745 nBrowsers++;
746 }
747 listString = updated.toString();
748 }
749 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
750 }
751
752 /**
753 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
754 */
755 protected void removePlayer() { }
Selim Cinek3df592e2020-04-28 13:51:43 -0700756
Selim Cinekf418bb02020-05-04 17:16:58 -0700757 public void measure(@Nullable MediaMeasurementInput input) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400758 if (mViewHolder == null) {
759 return;
760 }
Selim Cinekf418bb02020-05-04 17:16:58 -0700761 if (input != null) {
762 int width = input.getWidth();
763 setPlayerWidth(width);
Robert Snoebergereb49e942020-05-12 16:31:09 -0400764 mViewHolder.getPlayer().measure(input.getWidthMeasureSpec(),
765 input.getHeightMeasureSpec());
Selim Cinek3df592e2020-04-28 13:51:43 -0700766 }
767 }
768
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700769 public void setPlayerWidth(int width) {
Robert Snoebergereb49e942020-05-12 16:31:09 -0400770 if (mViewHolder == null) {
771 return;
772 }
773 MotionLayout view = mViewHolder.getPlayer();
774 ConstraintSet expandedSet = view.getConstraintSet(R.id.expanded);
775 ConstraintSet collapsedSet = view.getConstraintSet(R.id.collapsed);
Selim Cinek54809622020-04-30 19:04:44 -0700776 collapsedSet.setGuidelineBegin(R.id.view_width, width);
777 expandedSet.setGuidelineBegin(R.id.view_width, width);
Robert Snoebergereb49e942020-05-12 16:31:09 -0400778 view.updateState(R.id.collapsed, collapsedSet);
779 view.updateState(R.id.expanded, expandedSet);
Selim Cinek3df592e2020-04-28 13:51:43 -0700780 }
Selim Cinekf418bb02020-05-04 17:16:58 -0700781
782 public void animatePendingSizeChange(long duration, long startDelay) {
783 mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
784 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500785}