blob: 0b0bfc62ed747ca088a3dc6d6e54b3839d92de9d [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;
Selim Cinek3df592e2020-04-28 13:51:43 -070039import android.util.DisplayMetrics;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050040import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050041import android.view.LayoutInflater;
42import android.view.View;
43import android.view.ViewGroup;
44import android.widget.ImageButton;
45import android.widget.ImageView;
46import android.widget.LinearLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070047import android.widget.SeekBar;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050048import android.widget.TextView;
49
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040050import androidx.annotation.Nullable;
Selim Cinekf0f74952020-04-21 11:45:16 -070051import androidx.constraintlayout.motion.widget.Key;
52import androidx.constraintlayout.motion.widget.KeyAttributes;
53import androidx.constraintlayout.motion.widget.KeyFrames;
Selim Cinekd8357922020-04-10 15:06:53 -070054import androidx.constraintlayout.motion.widget.MotionLayout;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070055import androidx.constraintlayout.widget.ConstraintSet;
Selim Cinekc5436712020-04-27 15:15:44 -070056import androidx.core.graphics.drawable.RoundedBitmapDrawable;
57import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050058
Selim Cinekc5436712020-04-27 15:15:44 -070059import com.android.settingslib.Utils;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040060import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050061import com.android.settingslib.media.MediaDevice;
62import com.android.settingslib.media.MediaOutputSliceConstants;
63import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050064import com.android.systemui.R;
65import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040066import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040067import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070068import com.android.systemui.util.concurrency.DelayableExecutor;
69
70import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050071
Selim Cinekf0f74952020-04-21 11:45:16 -070072import java.util.ArrayList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050073import java.util.List;
74import java.util.concurrent.Executor;
75
76/**
Selim Cinekd8357922020-04-10 15:06:53 -070077 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050078 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040079public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050080 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040081 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070082
83 // Button IDs for QS controls
84 static final int[] ACTION_IDS = {
85 R.id.action0,
86 R.id.action1,
87 R.id.action2,
88 R.id.action3,
89 R.id.action4
90 };
91
92 private final SeekBarViewModel mSeekBarViewModel;
93 private final SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040094 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040095 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040096 private final ActivityStarter mActivityStarter;
Selim Cinek3df592e2020-04-28 13:51:43 -070097 private final LayoutAnimationHelper mLayoutAnimationHelper;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050098
99 private Context mContext;
Selim Cinekf0f74952020-04-21 11:45:16 -0700100 private MotionLayout mMediaNotifView;
101 private final View mBackground;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500102 private View mSeamless;
103 private MediaSession.Token mToken;
104 private MediaController mController;
105 private int mForegroundColor;
106 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400107 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400108 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400109 private boolean mIsRegistered = false;
Selim Cinekf0f74952020-04-21 11:45:16 -0700110 private final List<KeyFrames> mKeyFrames;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400111 private String mKey;
Selim Cinekc5436712020-04-27 15:15:44 -0700112 private int mAlbumArtSize;
113 private int mAlbumArtRadius;
Selim Cinek3df592e2020-04-28 13:51:43 -0700114 private int mViewWidth;
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 Cinekf0f74952020-04-21 11:45:16 -0700183 mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400184 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400185 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500186 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400187 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700188 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
189 mSeekBarObserver = new SeekBarObserver(getView());
Selim Cinek098baf42020-04-27 19:02:06 -0700190 // TODO: we should pause this whenever the screen is off / panel is collapsed etc.
191 mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700192 SeekBar bar = getView().findViewById(R.id.media_progress_bar);
193 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
194 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
Selim Cinekc5436712020-04-27 15:15:44 -0700195 loadDimens();
196 }
197
Selim Cinek098baf42020-04-27 19:02:06 -0700198 public void onDestroy() {
199 mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
200 makeInactive();
201 }
202
Selim Cinekc5436712020-04-27 15:15:44 -0700203 private void loadDimens() {
204 mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
205 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
206 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500207 }
208
209 /**
210 * Get the view used to display media controls
211 * @return the view
212 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700213 public MotionLayout getView() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500214 return mMediaNotifView;
215 }
216
217 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700218 * Sets the listening state of the player.
219 *
220 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
221 * unnecessary work when the QS panel is closed.
222 *
223 * @param listening True when player should be active. Otherwise, false.
224 */
225 public void setListening(boolean listening) {
226 mSeekBarViewModel.setListening(listening);
227 }
228
229 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500230 * Get the context
231 * @return context
232 */
233 public Context getContext() {
234 return mContext;
235 }
236
237 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700238 * Bind this view based on the data given
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500239 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700240 public void bind(@NotNull MediaData data) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700241 MediaSession.Token token = data.getToken();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700242 mForegroundColor = data.getForegroundColor();
243 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400244 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400245 if (mQSMediaBrowser != null) {
246 Log.d(TAG, "Disconnecting old media browser");
247 mQSMediaBrowser.disconnect();
248 mQSMediaBrowser = null;
249 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400250 mToken = token;
251 mServiceComponent = null;
252 mCheckedForResumption = false;
253 }
254
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500255 mController = new MediaController(mContext, mToken);
256
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400257 // Try to find a browser service component for this app
258 // TODO also check for a media button receiver intended for restarting (b/154127084)
259 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400260 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400261 if (mServiceComponent == null && !mCheckedForResumption) {
262 Log.d(TAG, "Checking for service component");
263 PackageManager pm = mContext.getPackageManager();
264 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
265 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700266 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400267 if (resumeInfo != null) {
268 for (ResolveInfo inf : resumeInfo) {
269 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
270 mBackgroundExecutor.execute(() ->
271 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
272 break;
273 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500274 }
275 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400276 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500277 }
278
279 mController.registerCallback(mSessionCallback);
280
Selim Cinekf0f74952020-04-21 11:45:16 -0700281 mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
282 ColorStateList.valueOf(mBackgroundColor));
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500283
284 // Click action
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700285 PendingIntent clickIntent = data.getClickIntent();
286 if (clickIntent != null) {
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400287 mMediaNotifView.setOnClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700288 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400289 });
290 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500291
Selim Cinekf0f74952020-04-21 11:45:16 -0700292 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
Selim Cinekc5436712020-04-27 15:15:44 -0700293 // TODO: migrate this to a view with rounded corners instead of baking the rounding
294 // into the bitmap
295 Drawable artwork = createRoundedBitmap(data.getArtwork());
296 albumView.setImageDrawable(artwork);
Selim Cinekf0f74952020-04-21 11:45:16 -0700297
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500298 // App icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700299 ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
300 // TODO: look at iconDrawable
301 Drawable iconDrawable = data.getAppIcon();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500302 iconDrawable.setTint(mForegroundColor);
303 appIcon.setImageDrawable(iconDrawable);
304
Selim Cinekf0f74952020-04-21 11:45:16 -0700305 // Song name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700306 TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
307 titleText.setText(data.getSong());
Selim Cinekf0f74952020-04-21 11:45:16 -0700308 titleText.setTextColor(data.getForegroundColor());
309
310 // App title
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700311 TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
312 appName.setText(data.getApp());
313 appName.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700314
315 // Artist name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700316 TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
317 artistText.setText(data.getArtist());
318 artistText.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700319
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500320 // Transfer chip
321 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400322 if (mSeamless != null) {
323 if (mLocalMediaManager != null) {
324 mSeamless.setVisibility(View.VISIBLE);
325 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
326 mSeamless.setOnClickListener(v -> {
327 final Intent intent = new Intent()
328 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
329 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
330 mController.getPackageName())
331 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
332 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
333 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
334 });
335 } else {
336 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
337 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500338 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400339 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
340 if (playbackInfo != null) {
341 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
342 } else {
343 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
344 mIsRemotePlayback = false;
345 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500346
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700347 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
348 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
349 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
350 // Media controls
351 int i = 0;
352 List<MediaAction> actionIcons = data.getActions();
353 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700354 int actionId = ACTION_IDS[i];
355 final ImageButton button = mMediaNotifView.findViewById(actionId);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700356 MediaAction mediaAction = actionIcons.get(i);
357 button.setImageDrawable(mediaAction.getDrawable());
358 button.setContentDescription(mediaAction.getContentDescription());
359 button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
360 PendingIntent actionIntent = mediaAction.getIntent();
361
Selim Cinekf0f74952020-04-21 11:45:16 -0700362 if (mBackground.getBackground() instanceof IlluminationDrawable) {
363 ((IlluminationDrawable) mBackground.getBackground())
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700364 .setupTouch(button, mMediaNotifView);
365 }
366
367 button.setOnClickListener(v -> {
368 if (actionIntent != null) {
369 try {
370 actionIntent.send();
371 } catch (PendingIntent.CanceledException e) {
372 e.printStackTrace();
373 }
374 }
375 });
376 boolean visibleInCompat = actionsWhenCollapsed.contains(i);
Selim Cinekf0f74952020-04-21 11:45:16 -0700377 updateKeyFrameVisibility(actionId, visibleInCompat);
378 collapsedSet.setVisibility(actionId,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700379 visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
Selim Cinekf0f74952020-04-21 11:45:16 -0700380 expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700381 }
382
383 // Hide any unused buttons
384 for (; i < ACTION_IDS.length; i++) {
385 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
386 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
387 }
388
389 // Seek Bar
390 final MediaController controller = new MediaController(getContext(), data.getToken());
391 mBackgroundExecutor.execute(
392 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
393
394 // Set up long press menu
395 // TODO: b/156036025 bring back media guts
396
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400397 makeActive();
Selim Cinekf0f74952020-04-21 11:45:16 -0700398 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400399
Selim Cinekc5436712020-04-27 15:15:44 -0700400 private Drawable createRoundedBitmap(Icon icon) {
401 if (icon == null) {
402 return null;
403 }
404 // Let's scale down the View, such that the content always nicely fills the view.
405 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
406 // ratios
407 Drawable drawable = icon.loadDrawable(mContext);
408 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
409 Rect bounds;
410 if (aspectRatio > 1.0f) {
411 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
412 } else {
413 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
414 }
415 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
416 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
417 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
418 bounds.offset((int) -offsetX,(int) -offsetY);
419 }
420 drawable.setBounds(bounds);
421 Bitmap scaled = Bitmap.createBitmap(mAlbumArtSize, mAlbumArtSize,
422 Bitmap.Config.ARGB_8888);
423 Canvas canvas = new Canvas(scaled);
424 drawable.draw(canvas);
425 RoundedBitmapDrawable artwork = RoundedBitmapDrawableFactory.create(
426 mContext.getResources(), scaled);
427 artwork.setCornerRadius(mAlbumArtRadius);
428 return artwork;
429 }
430
Selim Cinekf0f74952020-04-21 11:45:16 -0700431 /**
432 * Updates the keyframe visibility such that only views that are not visible actually go
433 * through a transition and fade in.
434 *
435 * @param actionId the id to change
436 * @param visible is the view visible
437 */
438 private void updateKeyFrameVisibility(int actionId, boolean visible) {
439 for (int i = 0; i < mKeyFrames.size(); i++) {
440 KeyFrames keyframe = mKeyFrames.get(i);
441 ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
442 for (int j = 0; j < viewKeyFrames.size(); j++) {
443 Key key = viewKeyFrames.get(j);
444 if (key instanceof KeyAttributes) {
445 KeyAttributes attributes = (KeyAttributes) key;
446 attributes.setValue("alpha", visible ? 1.0f : 0.0f);
447 }
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400448 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400449 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500450 }
451
452 /**
453 * Return the token for the current media session
454 * @return the token
455 */
456 public MediaSession.Token getMediaSessionToken() {
457 return mToken;
458 }
459
460 /**
461 * Get the current media controller
462 * @return the controller
463 */
464 public MediaController getController() {
465 return mController;
466 }
467
468 /**
469 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400470 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500471 */
472 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400473 if (mController == null) {
474 return null;
475 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500476 return mController.getPackageName();
477 }
478
479 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400480 * Return the original notification's key
481 * @return The notification key
482 */
483 public String getKey() {
484 return mKey;
485 }
486
487 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500488 * Check whether this player has an attached media session.
489 * @return whether there is a controller with a current media session.
490 */
491 public boolean hasMediaSession() {
492 return mController != null && mController.getPlaybackState() != null;
493 }
494
495 /**
496 * Check whether the media controlled by this player is currently playing
497 * @return whether it is playing, or false if no controller information
498 */
499 public boolean isPlaying() {
500 return isPlaying(mController);
501 }
502
503 /**
504 * Check whether the given controller is currently playing
505 * @param controller media controller to check
506 * @return whether it is playing, or false if no controller information
507 */
508 protected boolean isPlaying(MediaController controller) {
509 if (controller == null) {
510 return false;
511 }
512
513 PlaybackState state = controller.getPlaybackState();
514 if (state == null) {
515 return false;
516 }
517
518 return (state.getState() == PlaybackState.STATE_PLAYING);
519 }
520
521 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500522 * Update the current device information
523 * @param device device information to display
524 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400525 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500526 if (mSeamless == null) {
527 return;
528 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400529 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500530 updateChipInternal(device);
531 });
532 }
533
534 private void updateChipInternal(MediaDevice device) {
535 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
536
537 // Update the outline color
538 LinearLayout viewLayout = (LinearLayout) mSeamless;
539 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
540 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
541 rect.setStroke(2, mForegroundColor);
542 rect.setColor(mBackgroundColor);
543
544 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
545 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
546 deviceName.setTextColor(fgTintList);
547
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400548 if (mIsRemotePlayback) {
549 mSeamless.setEnabled(false);
550 mSeamless.setAlpha(0.38f);
551 iconView.setImageResource(R.drawable.ic_hardware_speaker);
552 iconView.setVisibility(View.VISIBLE);
553 iconView.setImageTintList(fgTintList);
554 deviceName.setText(R.string.media_seamless_remote_device);
555 } else if (device != null) {
556 mSeamless.setEnabled(true);
557 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500558 Drawable icon = device.getIcon();
559 iconView.setVisibility(View.VISIBLE);
560 iconView.setImageTintList(fgTintList);
561
562 if (icon instanceof AdaptiveIcon) {
563 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
564 aIcon.setBackgroundColor(mBackgroundColor);
565 iconView.setImageDrawable(aIcon);
566 } else {
567 iconView.setImageDrawable(icon);
568 }
569 deviceName.setText(device.getName());
570 } else {
571 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400572 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400573 mSeamless.setEnabled(true);
574 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500575 iconView.setVisibility(View.GONE);
576 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
577 }
578 }
579
580 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400581 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
582 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500583 */
584 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400585 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400586 if (mServiceComponent == null) {
587 // If we don't have a way to resume, just remove the player altogether
588 Log.d(TAG, "Removing unresumable controls");
589 removePlayer();
590 return;
591 }
592 resetButtons();
593 }
594
595 /**
596 * Hide the media buttons and show only a restart button
597 */
598 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500599 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700600
601 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
602 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
603 for (int i = 1; i < ACTION_IDS.length; i++) {
604 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
605 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500606 }
607
608 // Add a restart button
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700609 ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500610 btn.setOnClickListener(v -> {
611 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400612 if (mQSMediaBrowser != null) {
613 mQSMediaBrowser.disconnect();
614 }
615 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
616 @Override
617 public void onConnected() {
618 Log.d(TAG, "Successfully restarted");
619 }
620 @Override
621 public void onError() {
622 Log.e(TAG, "Error restarting");
623 mQSMediaBrowser.disconnect();
624 mQSMediaBrowser = null;
625 }
626 }, mServiceComponent);
627 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500628 });
629 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
630 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700631 expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
632 collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
633
634 mSeekBarViewModel.clearController();
635 // TODO: fix guts
636 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
637 View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
638
639 mMediaNotifView.setOnLongClickListener(v -> {
640 // Replace player view with close/cancel view
641// guts.setVisibility(View.GONE);
642 options.setVisibility(View.VISIBLE);
643 return true; // consumed click
644 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500645 }
646
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400647 private void makeActive() {
648 Assert.isMainThread();
649 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400650 if (mLocalMediaManager != null) {
651 mLocalMediaManager.registerCallback(mDeviceCallback);
652 mLocalMediaManager.startScan();
653 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400654 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500655 }
656 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400657
658 private void makeInactive() {
659 Assert.isMainThread();
660 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400661 if (mLocalMediaManager != null) {
662 mLocalMediaManager.stopScan();
663 mLocalMediaManager.unregisterCallback(mDeviceCallback);
664 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400665 mIsRegistered = false;
666 }
667 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400668 /**
669 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
670 * component to the list of resumption components
671 */
672 private void tryUpdateResumptionList(ComponentName componentName) {
673 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400674 if (mQSMediaBrowser != null) {
675 mQSMediaBrowser.disconnect();
676 }
677 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400678 new QSMediaBrowser.Callback() {
679 @Override
680 public void onConnected() {
681 Log.d(TAG, "yes we can resume with " + componentName);
682 mServiceComponent = componentName;
683 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400684 mQSMediaBrowser.disconnect();
685 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400686 }
687
688 @Override
689 public void onError() {
690 Log.d(TAG, "Cannot resume with " + componentName);
691 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400692 if (!hasMediaSession()) {
693 // If it's not active and we can't resume, remove
694 removePlayer();
695 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400696 mQSMediaBrowser.disconnect();
697 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400698 }
699 },
700 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400701 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400702 }
703
704 /**
705 * Add the component to the saved list of media browser services, checking for duplicates and
706 * removing older components that exceed the maximum limit
707 * @param componentName
708 */
709 private synchronized void updateResumptionList(ComponentName componentName) {
710 // Add to front of saved list
711 if (mSharedPrefs == null) {
712 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
713 }
714 String componentString = componentName.flattenToString();
715 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
716 if (listString == null) {
717 listString = componentString;
718 } else {
719 String[] components = listString.split(QSMediaBrowser.DELIMITER);
720 StringBuilder updated = new StringBuilder(componentString);
721 int nBrowsers = 1;
722 for (int i = 0; i < components.length
723 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
724 if (componentString.equals(components[i])) {
725 continue;
726 }
727 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
728 nBrowsers++;
729 }
730 listString = updated.toString();
731 }
732 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
733 }
734
735 /**
736 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
737 */
738 protected void removePlayer() { }
Selim Cinek3df592e2020-04-28 13:51:43 -0700739
740 public void setDimension(int newWidth, int newHeight, boolean animate, long duration,
741 long startDelay) {
742 // Let's remeasure if our width changed. Our height is dependent on the expansion, so we
743 // won't animate if it changed
744 if (newWidth != mViewWidth) {
745 if (animate) {
746 mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
747 }
748 setViewWidth(newWidth);
749 mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight());
750 }
751 }
752
753 protected void setViewWidth(int newWidth) {
754 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
755 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
756 collapsedSet.setGuidelineBegin(R.id.view_width, newWidth);
757 expandedSet.setGuidelineBegin(R.id.view_width, newWidth);
758 DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
759 int widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
760 View.MeasureSpec.AT_MOST);
761 int heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
762 View.MeasureSpec.AT_MOST);
763 mMediaNotifView.setMinimumWidth(displayMetrics.widthPixels);
764 mMediaNotifView.measure(widthSpec, heightSpec);
765 mViewWidth = newWidth;
766 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500767}