blob: 72282714365b795bacab170b4cd2ffbdc8529c2e [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.LayoutInflater;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.ImageButton;
44import android.widget.ImageView;
45import android.widget.LinearLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070046import android.widget.SeekBar;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050047import android.widget.TextView;
48
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040049import androidx.annotation.Nullable;
Selim Cinekf0f74952020-04-21 11:45:16 -070050import androidx.constraintlayout.motion.widget.Key;
51import androidx.constraintlayout.motion.widget.KeyAttributes;
52import androidx.constraintlayout.motion.widget.KeyFrames;
Selim Cinekd8357922020-04-10 15:06:53 -070053import androidx.constraintlayout.motion.widget.MotionLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070054import androidx.constraintlayout.widget.ConstraintSet;
Selim Cinekc5436712020-04-27 15:15:44 -070055import androidx.core.graphics.drawable.RoundedBitmapDrawable;
56import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050057
Selim Cinekc5436712020-04-27 15:15:44 -070058import com.android.settingslib.Utils;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040059import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050060import com.android.settingslib.media.MediaDevice;
61import com.android.settingslib.media.MediaOutputSliceConstants;
62import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050063import com.android.systemui.R;
64import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040065import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040066import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070067import com.android.systemui.util.concurrency.DelayableExecutor;
68
69import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050070
Selim Cinekf0f74952020-04-21 11:45:16 -070071import java.util.ArrayList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050072import java.util.List;
73import java.util.concurrent.Executor;
74
75/**
Selim Cinekd8357922020-04-10 15:06:53 -070076 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050077 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040078public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050079 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040080 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070081
82 // Button IDs for QS controls
83 static final int[] ACTION_IDS = {
84 R.id.action0,
85 R.id.action1,
86 R.id.action2,
87 R.id.action3,
88 R.id.action4
89 };
90
91 private final SeekBarViewModel mSeekBarViewModel;
92 private final SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040093 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040094 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040095 private final ActivityStarter mActivityStarter;
Selim Cinek3df592e2020-04-28 13:51:43 -070096 private final LayoutAnimationHelper mLayoutAnimationHelper;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050097
98 private Context mContext;
Selim Cinekf0f74952020-04-21 11:45:16 -070099 private MotionLayout mMediaNotifView;
100 private final View mBackground;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500101 private View mSeamless;
102 private MediaSession.Token mToken;
103 private MediaController mController;
104 private int mForegroundColor;
105 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400106 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400107 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400108 private boolean mIsRegistered = false;
Selim Cinekf0f74952020-04-21 11:45:16 -0700109 private final List<KeyFrames> mKeyFrames;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400110 private String mKey;
Selim Cinekc5436712020-04-27 15:15:44 -0700111 private int mAlbumArtSize;
112 private int mAlbumArtRadius;
Selim Cinek3df592e2020-04-28 13:51:43 -0700113 private int mViewWidth;
Selim Cinek54809622020-04-30 19:04:44 -0700114 private MediaMeasurementInput mLastMeasureInput;
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
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400142 private final LocalMediaManager.DeviceCallback mDeviceCallback =
143 new LocalMediaManager.DeviceCallback() {
144 @Override
145 public void onDeviceListUpdate(List<MediaDevice> devices) {
146 if (mLocalMediaManager == null) {
147 return;
148 }
149 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
150 // Check because this can be called several times while changing devices
151 if (mDevice == null || !mDevice.equals(currentDevice)) {
152 mDevice = currentDevice;
153 updateDevice(mDevice);
154 }
155 }
156
157 @Override
158 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
159 if (mDevice == null || !mDevice.equals(device)) {
160 mDevice = device;
161 updateDevice(mDevice);
162 }
163 }
164 };
165
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500166 /**
167 * Initialize a new control panel
168 * @param context
169 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400170 * @param routeManager Manager used to listen for device change events.
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400171 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500172 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400173 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500174 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400175 public MediaControlPanel(Context context, ViewGroup parent,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700176 @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
177 DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500178 mContext = context;
179 LayoutInflater inflater = LayoutInflater.from(mContext);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700180 mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
Selim Cinekf0f74952020-04-21 11:45:16 -0700181 mBackground = mMediaNotifView.findViewById(R.id.media_background);
Selim Cinek3df592e2020-04-28 13:51:43 -0700182 mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView);
Selim Cinek8081f092020-05-01 21:11:13 -0700183 GoneChildrenHideHelper.clipGoneChildrenOnLayout(mMediaNotifView);
Selim Cinekf0f74952020-04-21 11:45:16 -0700184 mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400185 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400186 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500187 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400188 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700189 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
190 mSeekBarObserver = new SeekBarObserver(getView());
Selim Cinek098baf42020-04-27 19:02:06 -0700191 // TODO: we should pause this whenever the screen is off / panel is collapsed etc.
192 mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700193 SeekBar bar = getView().findViewById(R.id.media_progress_bar);
194 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
195 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
Selim Cinekc5436712020-04-27 15:15:44 -0700196 loadDimens();
197 }
198
Selim Cinek098baf42020-04-27 19:02:06 -0700199 public void onDestroy() {
200 mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
201 makeInactive();
202 }
203
Selim Cinekc5436712020-04-27 15:15:44 -0700204 private void loadDimens() {
205 mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
206 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
207 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500208 }
209
210 /**
211 * Get the view used to display media controls
212 * @return the view
213 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700214 public MotionLayout getView() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500215 return mMediaNotifView;
216 }
217
218 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700219 * Sets the listening state of the player.
220 *
221 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
222 * unnecessary work when the QS panel is closed.
223 *
224 * @param listening True when player should be active. Otherwise, false.
225 */
226 public void setListening(boolean listening) {
227 mSeekBarViewModel.setListening(listening);
228 }
229
230 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500231 * Get the context
232 * @return context
233 */
234 public Context getContext() {
235 return mContext;
236 }
237
238 /**
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) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700242 MediaSession.Token token = data.getToken();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700243 mForegroundColor = data.getForegroundColor();
244 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400245 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400246 if (mQSMediaBrowser != null) {
247 Log.d(TAG, "Disconnecting old media browser");
248 mQSMediaBrowser.disconnect();
249 mQSMediaBrowser = null;
250 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400251 mToken = token;
252 mServiceComponent = null;
253 mCheckedForResumption = false;
254 }
255
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500256 mController = new MediaController(mContext, mToken);
257
Selim Cinek8081f092020-05-01 21:11:13 -0700258 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
259 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
260
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400261 // Try to find a browser service component for this app
262 // TODO also check for a media button receiver intended for restarting (b/154127084)
263 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400264 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400265 if (mServiceComponent == null && !mCheckedForResumption) {
266 Log.d(TAG, "Checking for service component");
267 PackageManager pm = mContext.getPackageManager();
268 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
269 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700270 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400271 if (resumeInfo != null) {
272 for (ResolveInfo inf : resumeInfo) {
273 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
274 mBackgroundExecutor.execute(() ->
275 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
276 break;
277 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500278 }
279 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400280 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500281 }
282
283 mController.registerCallback(mSessionCallback);
284
Selim Cinekf0f74952020-04-21 11:45:16 -0700285 mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
286 ColorStateList.valueOf(mBackgroundColor));
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500287
288 // Click action
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700289 PendingIntent clickIntent = data.getClickIntent();
290 if (clickIntent != null) {
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400291 mMediaNotifView.setOnClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700292 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400293 });
294 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500295
Selim Cinekf0f74952020-04-21 11:45:16 -0700296 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
Selim Cinekc5436712020-04-27 15:15:44 -0700297 // TODO: migrate this to a view with rounded corners instead of baking the rounding
298 // into the bitmap
299 Drawable artwork = createRoundedBitmap(data.getArtwork());
300 albumView.setImageDrawable(artwork);
Selim Cinekf0f74952020-04-21 11:45:16 -0700301
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500302 // App icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700303 ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
304 // TODO: look at iconDrawable
305 Drawable iconDrawable = data.getAppIcon();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500306 iconDrawable.setTint(mForegroundColor);
307 appIcon.setImageDrawable(iconDrawable);
308
Selim Cinekf0f74952020-04-21 11:45:16 -0700309 // Song name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700310 TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
311 titleText.setText(data.getSong());
Selim Cinekf0f74952020-04-21 11:45:16 -0700312 titleText.setTextColor(data.getForegroundColor());
313
314 // App title
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700315 TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
316 appName.setText(data.getApp());
317 appName.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700318
319 // Artist name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700320 TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
321 artistText.setText(data.getArtist());
322 artistText.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700323
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500324 // Transfer chip
325 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400326 if (mSeamless != null) {
327 if (mLocalMediaManager != null) {
328 mSeamless.setVisibility(View.VISIBLE);
Selim Cinek8081f092020-05-01 21:11:13 -0700329 setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
330 setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
Robert Snoeberger44299172020-04-24 22:22:21 -0400331 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
332 mSeamless.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);
343 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500344 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400345 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
346 if (playbackInfo != null) {
347 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
348 } else {
349 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
350 mIsRemotePlayback = false;
351 }
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700352 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
353 // Media controls
354 int i = 0;
355 List<MediaAction> actionIcons = data.getActions();
356 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700357 int actionId = ACTION_IDS[i];
358 final ImageButton button = mMediaNotifView.findViewById(actionId);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700359 MediaAction mediaAction = actionIcons.get(i);
360 button.setImageDrawable(mediaAction.getDrawable());
361 button.setContentDescription(mediaAction.getContentDescription());
362 button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
363 PendingIntent actionIntent = mediaAction.getIntent();
364
Selim Cinekf0f74952020-04-21 11:45:16 -0700365 if (mBackground.getBackground() instanceof IlluminationDrawable) {
366 ((IlluminationDrawable) mBackground.getBackground())
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700367 .setupTouch(button, mMediaNotifView);
368 }
369
370 button.setOnClickListener(v -> {
371 if (actionIntent != null) {
372 try {
373 actionIntent.send();
374 } catch (PendingIntent.CanceledException e) {
375 e.printStackTrace();
376 }
377 }
378 });
379 boolean visibleInCompat = actionsWhenCollapsed.contains(i);
Selim Cinekf0f74952020-04-21 11:45:16 -0700380 updateKeyFrameVisibility(actionId, visibleInCompat);
Selim Cinek8081f092020-05-01 21:11:13 -0700381 setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
382 setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700383 }
384
385 // Hide any unused buttons
386 for (; i < ACTION_IDS.length; i++) {
Selim Cinek8081f092020-05-01 21:11:13 -0700387 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
388 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700389 }
390
391 // Seek Bar
392 final MediaController controller = new MediaController(getContext(), data.getToken());
393 mBackgroundExecutor.execute(
394 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
395
396 // Set up long press menu
397 // TODO: b/156036025 bring back media guts
398
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400399 makeActive();
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700400 mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
401 mMediaNotifView.updateState(R.id.expanded, expandedSet);
Selim Cinekf0f74952020-04-21 11:45:16 -0700402 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400403
Selim Cinekc5436712020-04-27 15:15:44 -0700404 private Drawable createRoundedBitmap(Icon icon) {
405 if (icon == null) {
406 return null;
407 }
408 // Let's scale down the View, such that the content always nicely fills the view.
409 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
410 // ratios
411 Drawable drawable = icon.loadDrawable(mContext);
412 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
413 Rect bounds;
414 if (aspectRatio > 1.0f) {
415 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
416 } else {
417 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
418 }
419 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
420 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
421 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
422 bounds.offset((int) -offsetX,(int) -offsetY);
423 }
424 drawable.setBounds(bounds);
425 Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize,
426 Bitmap.Config.ARGB_8888);
427 Canvas canvas = new Canvas(scaled);
428 drawable.draw(canvas);
429 RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create(
430 mContext.getResources(), scaled);
431 artwork.setCornerRadius(mAlbumArtRadius);
432 return artwork;
433 }
434
Selim Cinekf0f74952020-04-21 11:45:16 -0700435 /**
436 * Updates the keyframe visibility such that only views that are not visible actually go
437 * through a transition and fade in.
438 *
439 * @param actionId the id to change
440 * @param visible is the view visible
441 */
442 private void updateKeyFrameVisibility(int actionId, boolean visible) {
443 for (int i = 0; i < mKeyFrames.size(); i++) {
444 KeyFrames keyframe = mKeyFrames.get(i);
445 ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
446 for (int j = 0; j < viewKeyFrames.size(); j++) {
447 Key key = viewKeyFrames.get(j);
448 if (key instanceof KeyAttributes) {
449 KeyAttributes attributes = (KeyAttributes) key;
450 attributes.setValue("alpha", visible ? 1.0f : 0.0f);
451 }
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400452 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400453 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500454 }
455
456 /**
457 * Return the token for the current media session
458 * @return the token
459 */
460 public MediaSession.Token getMediaSessionToken() {
461 return mToken;
462 }
463
464 /**
465 * Get the current media controller
466 * @return the controller
467 */
468 public MediaController getController() {
469 return mController;
470 }
471
472 /**
473 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400474 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500475 */
476 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400477 if (mController == null) {
478 return null;
479 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500480 return mController.getPackageName();
481 }
482
483 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400484 * Return the original notification's key
485 * @return The notification key
486 */
487 public String getKey() {
488 return mKey;
489 }
490
491 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500492 * Check whether this player has an attached media session.
493 * @return whether there is a controller with a current media session.
494 */
495 public boolean hasMediaSession() {
496 return mController != null && mController.getPlaybackState() != null;
497 }
498
499 /**
500 * Check whether the media controlled by this player is currently playing
501 * @return whether it is playing, or false if no controller information
502 */
503 public boolean isPlaying() {
504 return isPlaying(mController);
505 }
506
507 /**
508 * Check whether the given controller is currently playing
509 * @param controller media controller to check
510 * @return whether it is playing, or false if no controller information
511 */
512 protected boolean isPlaying(MediaController controller) {
513 if (controller == null) {
514 return false;
515 }
516
517 PlaybackState state = controller.getPlaybackState();
518 if (state == null) {
519 return false;
520 }
521
522 return (state.getState() == PlaybackState.STATE_PLAYING);
523 }
524
525 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500526 * Update the current device information
527 * @param device device information to display
528 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400529 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500530 if (mSeamless == null) {
531 return;
532 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400533 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500534 updateChipInternal(device);
535 });
536 }
537
538 private void updateChipInternal(MediaDevice device) {
539 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
540
541 // Update the outline color
542 LinearLayout viewLayout = (LinearLayout) mSeamless;
543 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
544 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
545 rect.setStroke(2, mForegroundColor);
546 rect.setColor(mBackgroundColor);
547
548 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
549 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
550 deviceName.setTextColor(fgTintList);
551
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400552 if (mIsRemotePlayback) {
553 mSeamless.setEnabled(false);
554 mSeamless.setAlpha(0.38f);
555 iconView.setImageResource(R.drawable.ic_hardware_speaker);
556 iconView.setVisibility(View.VISIBLE);
557 iconView.setImageTintList(fgTintList);
558 deviceName.setText(R.string.media_seamless_remote_device);
559 } else if (device != null) {
560 mSeamless.setEnabled(true);
561 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500562 Drawable icon = device.getIcon();
563 iconView.setVisibility(View.VISIBLE);
564 iconView.setImageTintList(fgTintList);
565
566 if (icon instanceof AdaptiveIcon) {
567 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
568 aIcon.setBackgroundColor(mBackgroundColor);
569 iconView.setImageDrawable(aIcon);
570 } else {
571 iconView.setImageDrawable(icon);
572 }
573 deviceName.setText(device.getName());
574 } else {
575 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400576 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400577 mSeamless.setEnabled(true);
578 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500579 iconView.setVisibility(View.GONE);
580 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
581 }
582 }
583
584 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400585 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
586 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500587 */
588 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400589 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400590 if (mServiceComponent == null) {
591 // If we don't have a way to resume, just remove the player altogether
592 Log.d(TAG, "Removing unresumable controls");
593 removePlayer();
594 return;
595 }
596 resetButtons();
597 }
598
599 /**
600 * Hide the media buttons and show only a restart button
601 */
602 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500603 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700604
605 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
606 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
607 for (int i = 1; i < ACTION_IDS.length; i++) {
Selim Cinek8081f092020-05-01 21:11:13 -0700608 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
609 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500610 }
611
612 // Add a restart button
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700613 ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500614 btn.setOnClickListener(v -> {
615 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400616 if (mQSMediaBrowser != null) {
617 mQSMediaBrowser.disconnect();
618 }
619 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
620 @Override
621 public void onConnected() {
622 Log.d(TAG, "Successfully restarted");
623 }
624 @Override
625 public void onError() {
626 Log.e(TAG, "Error restarting");
627 mQSMediaBrowser.disconnect();
628 mQSMediaBrowser = null;
629 }
630 }, mServiceComponent);
631 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500632 });
633 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
634 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek8081f092020-05-01 21:11:13 -0700635 setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
636 setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700637
638 mSeekBarViewModel.clearController();
639 // TODO: fix guts
640 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
641 View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
642
643 mMediaNotifView.setOnLongClickListener(v -> {
644 // Replace player view with close/cancel view
645// guts.setVisibility(View.GONE);
646 options.setVisibility(View.VISIBLE);
647 return true; // consumed click
648 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500649 }
650
Selim Cinek8081f092020-05-01 21:11:13 -0700651 private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
652 set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
653 set.setAlpha(actionId, visible ? 1.0f : 0.0f);
654 }
655
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400656 private void makeActive() {
657 Assert.isMainThread();
658 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400659 if (mLocalMediaManager != null) {
660 mLocalMediaManager.registerCallback(mDeviceCallback);
661 mLocalMediaManager.startScan();
662 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400663 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500664 }
665 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400666
667 private void makeInactive() {
668 Assert.isMainThread();
669 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400670 if (mLocalMediaManager != null) {
671 mLocalMediaManager.stopScan();
672 mLocalMediaManager.unregisterCallback(mDeviceCallback);
673 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400674 mIsRegistered = false;
675 }
676 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400677 /**
678 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
679 * component to the list of resumption components
680 */
681 private void tryUpdateResumptionList(ComponentName componentName) {
682 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400683 if (mQSMediaBrowser != null) {
684 mQSMediaBrowser.disconnect();
685 }
686 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400687 new QSMediaBrowser.Callback() {
688 @Override
689 public void onConnected() {
690 Log.d(TAG, "yes we can resume with " + componentName);
691 mServiceComponent = componentName;
692 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400693 mQSMediaBrowser.disconnect();
694 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400695 }
696
697 @Override
698 public void onError() {
699 Log.d(TAG, "Cannot resume with " + componentName);
700 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400701 if (!hasMediaSession()) {
702 // If it's not active and we can't resume, remove
703 removePlayer();
704 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400705 mQSMediaBrowser.disconnect();
706 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400707 }
708 },
709 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400710 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400711 }
712
713 /**
714 * Add the component to the saved list of media browser services, checking for duplicates and
715 * removing older components that exceed the maximum limit
716 * @param componentName
717 */
718 private synchronized void updateResumptionList(ComponentName componentName) {
719 // Add to front of saved list
720 if (mSharedPrefs == null) {
721 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
722 }
723 String componentString = componentName.flattenToString();
724 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
725 if (listString == null) {
726 listString = componentString;
727 } else {
728 String[] components = listString.split(QSMediaBrowser.DELIMITER);
729 StringBuilder updated = new StringBuilder(componentString);
730 int nBrowsers = 1;
731 for (int i = 0; i < components.length
732 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
733 if (componentString.equals(components[i])) {
734 continue;
735 }
736 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
737 nBrowsers++;
738 }
739 listString = updated.toString();
740 }
741 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
742 }
743
744 /**
745 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
746 */
747 protected void removePlayer() { }
Selim Cinek3df592e2020-04-28 13:51:43 -0700748
Selim Cinek54809622020-04-30 19:04:44 -0700749 public void remeasure(@Nullable MediaMeasurementInput input, boolean animate, long duration,
Selim Cinek3df592e2020-04-28 13:51:43 -0700750 long startDelay) {
751 // Let's remeasure if our width changed. Our height is dependent on the expansion, so we
752 // won't animate if it changed
Selim Cinek54809622020-04-30 19:04:44 -0700753 if (input != null && !input.sameAs(mLastMeasureInput)) {
754 mLastMeasureInput = input;
Selim Cinek3df592e2020-04-28 13:51:43 -0700755 if (animate) {
756 mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
757 }
Selim Cinek54809622020-04-30 19:04:44 -0700758 remeasureInternal(input);
759 mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(),
760 mMediaNotifView.getMeasuredHeight());
Selim Cinek3df592e2020-04-28 13:51:43 -0700761 }
762 }
763
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700764 private void remeasureInternal(MediaMeasurementInput input) {
765 int width = input.getWidth();
766 setPlayerWidth(width);
767 mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
Selim Cinek54809622020-04-30 19:04:44 -0700768 }
769
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700770 public void setPlayerWidth(int width) {
Selim Cinek3df592e2020-04-28 13:51:43 -0700771 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
772 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
Selim Cinek54809622020-04-30 19:04:44 -0700773 collapsedSet.setGuidelineBegin(R.id.view_width, width);
774 expandedSet.setGuidelineBegin(R.id.view_width, width);
Selim Cinekb28ec0a2020-05-01 15:07:42 -0700775 mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
776 mMediaNotifView.updateState(R.id.expanded, expandedSet);
Selim Cinek3df592e2020-04-28 13:51:43 -0700777 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500778}