blob: 673fafa0b1a3a6e6d06dbcb2fae2eba2ab52632b [file] [log] [blame]
Beth Thibodeau7b6c1782020-03-05 11:43:51 -05001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.media;
18
Selim Cinek5dbef2d2020-05-07 17:44:38 -070019import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
20
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050021import android.app.PendingIntent;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040025import android.content.SharedPreferences;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050026import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.content.res.ColorStateList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050029import android.graphics.drawable.Drawable;
30import android.graphics.drawable.GradientDrawable;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050031import android.graphics.drawable.RippleDrawable;
32import android.media.MediaMetadata;
33import android.media.session.MediaController;
Robert Snoebergerc981dc92020-04-27 15:00:50 -040034import android.media.session.MediaController.PlaybackInfo;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050035import android.media.session.MediaSession;
36import android.media.session.PlaybackState;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040037import android.service.media.MediaBrowserService;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050038import android.util.Log;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050039import android.view.LayoutInflater;
40import android.view.View;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040041import android.view.View.OnAttachStateChangeListener;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050042import 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;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050055
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040056import com.android.settingslib.media.LocalMediaManager;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050057import com.android.settingslib.media.MediaDevice;
58import com.android.settingslib.media.MediaOutputSliceConstants;
59import com.android.settingslib.widget.AdaptiveIcon;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050060import com.android.systemui.R;
61import com.android.systemui.plugins.ActivityStarter;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040062import com.android.systemui.qs.QSMediaBrowser;
Robert Snoeberger3cc22222020-03-25 15:36:31 -040063import com.android.systemui.util.Assert;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070064import com.android.systemui.util.concurrency.DelayableExecutor;
65
66import org.jetbrains.annotations.NotNull;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050067
Selim Cinekf0f74952020-04-21 11:45:16 -070068import java.util.ArrayList;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050069import java.util.List;
70import java.util.concurrent.Executor;
71
72/**
Selim Cinekd8357922020-04-10 15:06:53 -070073 * A view controller used for Media Playback.
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050074 */
Robert Snoeberger3cc22222020-03-25 15:36:31 -040075public class MediaControlPanel {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050076 private static final String TAG = "MediaControlPanel";
Robert Snoeberger9a7409b2020-04-09 18:12:27 -040077 @Nullable private final LocalMediaManager mLocalMediaManager;
Selim Cinek5dbef2d2020-05-07 17:44:38 -070078
79 // Button IDs for QS controls
80 static final int[] ACTION_IDS = {
81 R.id.action0,
82 R.id.action1,
83 R.id.action2,
84 R.id.action3,
85 R.id.action4
86 };
87
88 private final SeekBarViewModel mSeekBarViewModel;
89 private final SeekBarObserver mSeekBarObserver;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040090 private final Executor mForegroundExecutor;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -040091 protected final Executor mBackgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -040092 private final ActivityStarter mActivityStarter;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050093
94 private Context mContext;
Selim Cinekf0f74952020-04-21 11:45:16 -070095 private MotionLayout mMediaNotifView;
96 private final View mBackground;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050097 private View mSeamless;
98 private MediaSession.Token mToken;
99 private MediaController mController;
100 private int mForegroundColor;
101 private int mBackgroundColor;
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400102 private MediaDevice mDevice;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400103 protected ComponentName mServiceComponent;
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400104 private boolean mIsRegistered = false;
Selim Cinekf0f74952020-04-21 11:45:16 -0700105 private final List<KeyFrames> mKeyFrames;
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400106 private String mKey;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500107
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400108 public static final String MEDIA_PREFERENCES = "media_control_prefs";
109 public static final String MEDIA_PREFERENCE_KEY = "browser_components";
110 private SharedPreferences mSharedPrefs;
111 private boolean mCheckedForResumption = false;
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400112 private boolean mIsRemotePlayback;
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400113 private QSMediaBrowser mQSMediaBrowser;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400114
Beth Thibodeaud3ad81d2020-04-20 23:26:25 -0400115 // URI fields to try loading album art from
116 private static final String[] ART_URIS = {
117 MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
118 MediaMetadata.METADATA_KEY_ART_URI,
119 MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
120 };
121
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400122 private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500123 @Override
124 public void onSessionDestroyed() {
125 Log.d(TAG, "session destroyed");
126 mController.unregisterCallback(mSessionCallback);
127 clearControls();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400128 makeInactive();
129 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400130 @Override
Robert Snoeberger445d4412020-04-15 00:03:13 -0400131 public void onPlaybackStateChanged(PlaybackState state) {
132 final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
Beth Thibodeaud664de22020-04-28 16:29:36 -0400133 if (s == PlaybackState.STATE_NONE) {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400134 Log.d(TAG, "playback state change will trigger resumption, state=" + state);
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400135 clearControls();
136 makeInactive();
137 }
138 }
139 };
140
141 private final OnAttachStateChangeListener mStateListener = new OnAttachStateChangeListener() {
142 @Override
143 public void onViewAttachedToWindow(View unused) {
144 makeActive();
145 }
146 @Override
147 public void onViewDetachedFromWindow(View unused) {
148 makeInactive();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500149 }
150 };
151
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400152 private final LocalMediaManager.DeviceCallback mDeviceCallback =
153 new LocalMediaManager.DeviceCallback() {
154 @Override
155 public void onDeviceListUpdate(List<MediaDevice> devices) {
156 if (mLocalMediaManager == null) {
157 return;
158 }
159 MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
160 // Check because this can be called several times while changing devices
161 if (mDevice == null || !mDevice.equals(currentDevice)) {
162 mDevice = currentDevice;
163 updateDevice(mDevice);
164 }
165 }
166
167 @Override
168 public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
169 if (mDevice == null || !mDevice.equals(device)) {
170 mDevice = device;
171 updateDevice(mDevice);
172 }
173 }
174 };
175
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500176 /**
177 * Initialize a new control panel
178 * @param context
179 * @param parent
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400180 * @param routeManager Manager used to listen for device change events.
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400181 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500182 * @param backgroundExecutor background executor, used for processing artwork
Beth Thibodeaue561c002020-04-23 17:33:00 -0400183 * @param activityStarter activity starter
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500184 */
Robert Snoeberger445d4412020-04-15 00:03:13 -0400185 public MediaControlPanel(Context context, ViewGroup parent,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700186 @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
187 DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500188 mContext = context;
189 LayoutInflater inflater = LayoutInflater.from(mContext);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700190 mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
Selim Cinekf0f74952020-04-21 11:45:16 -0700191 mBackground = mMediaNotifView.findViewById(R.id.media_background);
192 mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400193 // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
194 // mStateListener shouldn't need to be unregistered since this object shares the same
195 // lifecycle with the inflated view. It would be better, however, if this controller used an
196 // attach/detach of views instead of inflating them in the constructor, which would allow
197 // mStateListener to be unregistered in detach.
198 mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400199 mLocalMediaManager = routeManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400200 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500201 mBackgroundExecutor = backgroundExecutor;
Beth Thibodeaue561c002020-04-23 17:33:00 -0400202 mActivityStarter = activityStarter;
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700203 mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
204 mSeekBarObserver = new SeekBarObserver(getView());
205 // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
206 // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
207 // will stop updating. So, use the lifecycle of the parent instead.
208 // TODO: this parent is also detached, need to fix that
209 mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
210 SeekBar bar = getView().findViewById(R.id.media_progress_bar);
211 bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
212 bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500213 }
214
215 /**
216 * Get the view used to display media controls
217 * @return the view
218 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700219 public MotionLayout getView() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500220 return mMediaNotifView;
221 }
222
223 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700224 * Sets the listening state of the player.
225 *
226 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
227 * unnecessary work when the QS panel is closed.
228 *
229 * @param listening True when player should be active. Otherwise, false.
230 */
231 public void setListening(boolean listening) {
232 mSeekBarViewModel.setListening(listening);
233 }
234
235 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500236 * Get the context
237 * @return context
238 */
239 public Context getContext() {
240 return mContext;
241 }
242
243 /**
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700244 * Bind this view based on the data given
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500245 */
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700246 public void bind(@NotNull MediaData data) {
Selim Cinekf0f74952020-04-21 11:45:16 -0700247 MediaSession.Token token = data.getToken();
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700248 mForegroundColor = data.getForegroundColor();
249 mBackgroundColor = data.getBackgroundColor();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400250 if (mToken == null || !mToken.equals(token)) {
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400251 if (mQSMediaBrowser != null) {
252 Log.d(TAG, "Disconnecting old media browser");
253 mQSMediaBrowser.disconnect();
254 mQSMediaBrowser = null;
255 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400256 mToken = token;
257 mServiceComponent = null;
258 mCheckedForResumption = false;
259 }
260
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500261 mController = new MediaController(mContext, mToken);
262
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400263 // Try to find a browser service component for this app
264 // TODO also check for a media button receiver intended for restarting (b/154127084)
265 // Only check if we haven't tried yet or the session token changed
Robert Snoeberger44299172020-04-24 22:22:21 -0400266 final String pkgName = mController.getPackageName();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400267 if (mServiceComponent == null && !mCheckedForResumption) {
268 Log.d(TAG, "Checking for service component");
269 PackageManager pm = mContext.getPackageManager();
270 Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
271 List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700272 // TODO: look into this resumption
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400273 if (resumeInfo != null) {
274 for (ResolveInfo inf : resumeInfo) {
275 if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
276 mBackgroundExecutor.execute(() ->
277 tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
278 break;
279 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500280 }
281 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400282 mCheckedForResumption = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500283 }
284
285 mController.registerCallback(mSessionCallback);
286
Selim Cinekf0f74952020-04-21 11:45:16 -0700287 mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
288 ColorStateList.valueOf(mBackgroundColor));
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500289
290 // Click action
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700291 PendingIntent clickIntent = data.getClickIntent();
292 if (clickIntent != null) {
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400293 mMediaNotifView.setOnClickListener(v -> {
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700294 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400295 });
296 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500297
Selim Cinekf0f74952020-04-21 11:45:16 -0700298 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
299 albumView.setImageDrawable(data.getArtwork());
300
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500301 // App icon
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700302 ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
303 // TODO: look at iconDrawable
304 Drawable iconDrawable = data.getAppIcon();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500305 iconDrawable.setTint(mForegroundColor);
306 appIcon.setImageDrawable(iconDrawable);
307
Selim Cinekf0f74952020-04-21 11:45:16 -0700308 // Song name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700309 TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
310 titleText.setText(data.getSong());
Selim Cinekf0f74952020-04-21 11:45:16 -0700311 titleText.setTextColor(data.getForegroundColor());
312
313 // App title
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700314 TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
315 appName.setText(data.getApp());
316 appName.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700317
318 // Artist name
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700319 TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
320 artistText.setText(data.getArtist());
321 artistText.setTextColor(mForegroundColor);
Selim Cinekf0f74952020-04-21 11:45:16 -0700322
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500323 // Transfer chip
324 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
Robert Snoeberger44299172020-04-24 22:22:21 -0400325 if (mSeamless != null) {
326 if (mLocalMediaManager != null) {
327 mSeamless.setVisibility(View.VISIBLE);
328 updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
329 mSeamless.setOnClickListener(v -> {
330 final Intent intent = new Intent()
331 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
332 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
333 mController.getPackageName())
334 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
335 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
336 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
337 });
338 } else {
339 Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
340 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500341 }
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400342 PlaybackInfo playbackInfo = mController.getPlaybackInfo();
343 if (playbackInfo != null) {
344 mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
345 } else {
346 Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
347 mIsRemotePlayback = false;
348 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500349
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700350 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
351 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
352 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);
381 collapsedSet.setVisibility(actionId,
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700382 visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
Selim Cinekf0f74952020-04-21 11:45:16 -0700383 expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE);
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700384 }
385
386 // Hide any unused buttons
387 for (; i < ACTION_IDS.length; i++) {
388 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
389 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
390 }
391
392 // Seek Bar
393 final MediaController controller = new MediaController(getContext(), data.getToken());
394 mBackgroundExecutor.execute(
395 () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
396
397 // Set up long press menu
398 // TODO: b/156036025 bring back media guts
399
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400400 makeActive();
Selim Cinekf0f74952020-04-21 11:45:16 -0700401 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400402
Selim Cinekf0f74952020-04-21 11:45:16 -0700403 /**
404 * Updates the keyframe visibility such that only views that are not visible actually go
405 * through a transition and fade in.
406 *
407 * @param actionId the id to change
408 * @param visible is the view visible
409 */
410 private void updateKeyFrameVisibility(int actionId, boolean visible) {
411 for (int i = 0; i < mKeyFrames.size(); i++) {
412 KeyFrames keyframe = mKeyFrames.get(i);
413 ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
414 for (int j = 0; j < viewKeyFrames.size(); j++) {
415 Key key = viewKeyFrames.get(j);
416 if (key instanceof KeyAttributes) {
417 KeyAttributes attributes = (KeyAttributes) key;
418 attributes.setValue("alpha", visible ? 1.0f : 0.0f);
419 }
Beth Thibodeau4b5ec832020-04-30 19:43:37 -0400420 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400421 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500422 }
423
424 /**
425 * Return the token for the current media session
426 * @return the token
427 */
428 public MediaSession.Token getMediaSessionToken() {
429 return mToken;
430 }
431
432 /**
433 * Get the current media controller
434 * @return the controller
435 */
436 public MediaController getController() {
437 return mController;
438 }
439
440 /**
441 * Get the name of the package associated with the current media controller
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400442 * @return the package name, or null if no controller
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500443 */
444 public String getMediaPlayerPackage() {
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400445 if (mController == null) {
446 return null;
447 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500448 return mController.getPackageName();
449 }
450
451 /**
Beth Thibodeaua3d90982020-04-13 23:42:48 -0400452 * Return the original notification's key
453 * @return The notification key
454 */
455 public String getKey() {
456 return mKey;
457 }
458
459 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500460 * Check whether this player has an attached media session.
461 * @return whether there is a controller with a current media session.
462 */
463 public boolean hasMediaSession() {
464 return mController != null && mController.getPlaybackState() != null;
465 }
466
467 /**
468 * Check whether the media controlled by this player is currently playing
469 * @return whether it is playing, or false if no controller information
470 */
471 public boolean isPlaying() {
472 return isPlaying(mController);
473 }
474
475 /**
476 * Check whether the given controller is currently playing
477 * @param controller media controller to check
478 * @return whether it is playing, or false if no controller information
479 */
480 protected boolean isPlaying(MediaController controller) {
481 if (controller == null) {
482 return false;
483 }
484
485 PlaybackState state = controller.getPlaybackState();
486 if (state == null) {
487 return false;
488 }
489
490 return (state.getState() == PlaybackState.STATE_PLAYING);
491 }
492
493 /**
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500494 * Update the current device information
495 * @param device device information to display
496 */
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400497 private void updateDevice(MediaDevice device) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500498 if (mSeamless == null) {
499 return;
500 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400501 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500502 updateChipInternal(device);
503 });
504 }
505
506 private void updateChipInternal(MediaDevice device) {
507 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
508
509 // Update the outline color
510 LinearLayout viewLayout = (LinearLayout) mSeamless;
511 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
512 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
513 rect.setStroke(2, mForegroundColor);
514 rect.setColor(mBackgroundColor);
515
516 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
517 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
518 deviceName.setTextColor(fgTintList);
519
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400520 if (mIsRemotePlayback) {
521 mSeamless.setEnabled(false);
522 mSeamless.setAlpha(0.38f);
523 iconView.setImageResource(R.drawable.ic_hardware_speaker);
524 iconView.setVisibility(View.VISIBLE);
525 iconView.setImageTintList(fgTintList);
526 deviceName.setText(R.string.media_seamless_remote_device);
527 } else if (device != null) {
528 mSeamless.setEnabled(true);
529 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500530 Drawable icon = device.getIcon();
531 iconView.setVisibility(View.VISIBLE);
532 iconView.setImageTintList(fgTintList);
533
534 if (icon instanceof AdaptiveIcon) {
535 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
536 aIcon.setBackgroundColor(mBackgroundColor);
537 iconView.setImageDrawable(aIcon);
538 } else {
539 iconView.setImageDrawable(icon);
540 }
541 deviceName.setText(device.getName());
542 } else {
543 // Reset to default
Robert Snoeberger44299172020-04-24 22:22:21 -0400544 Log.d(TAG, "device is null. Not binding output chip.");
Robert Snoebergerc981dc92020-04-27 15:00:50 -0400545 mSeamless.setEnabled(true);
546 mSeamless.setAlpha(1f);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500547 iconView.setVisibility(View.GONE);
548 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
549 }
550 }
551
552 /**
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400553 * Puts controls into a resumption state if possible, or calls removePlayer if no component was
554 * found that could resume playback
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500555 */
556 public void clearControls() {
Robert Snoeberger445d4412020-04-15 00:03:13 -0400557 Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400558 if (mServiceComponent == null) {
559 // If we don't have a way to resume, just remove the player altogether
560 Log.d(TAG, "Removing unresumable controls");
561 removePlayer();
562 return;
563 }
564 resetButtons();
565 }
566
567 /**
568 * Hide the media buttons and show only a restart button
569 */
570 protected void resetButtons() {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500571 // Hide all the old buttons
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700572
573 ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
574 ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
575 for (int i = 1; i < ACTION_IDS.length; i++) {
576 expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
577 collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500578 }
579
580 // Add a restart button
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700581 ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500582 btn.setOnClickListener(v -> {
583 Log.d(TAG, "Attempting to restart session");
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400584 if (mQSMediaBrowser != null) {
585 mQSMediaBrowser.disconnect();
586 }
587 mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
588 @Override
589 public void onConnected() {
590 Log.d(TAG, "Successfully restarted");
591 }
592 @Override
593 public void onError() {
594 Log.e(TAG, "Error restarting");
595 mQSMediaBrowser.disconnect();
596 mQSMediaBrowser = null;
597 }
598 }, mServiceComponent);
599 mQSMediaBrowser.restart();
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500600 });
601 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
602 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
Selim Cinek5dbef2d2020-05-07 17:44:38 -0700603 expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
604 collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
605
606 mSeekBarViewModel.clearController();
607 // TODO: fix guts
608 // View guts = mMediaNotifView.findViewById(R.id.media_guts);
609 View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
610
611 mMediaNotifView.setOnLongClickListener(v -> {
612 // Replace player view with close/cancel view
613// guts.setVisibility(View.GONE);
614 options.setVisibility(View.VISIBLE);
615 return true; // consumed click
616 });
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500617 }
618
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400619 private void makeActive() {
620 Assert.isMainThread();
621 if (!mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400622 if (mLocalMediaManager != null) {
623 mLocalMediaManager.registerCallback(mDeviceCallback);
624 mLocalMediaManager.startScan();
625 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400626 mIsRegistered = true;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500627 }
628 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400629
630 private void makeInactive() {
631 Assert.isMainThread();
632 if (mIsRegistered) {
Robert Snoeberger9a7409b2020-04-09 18:12:27 -0400633 if (mLocalMediaManager != null) {
634 mLocalMediaManager.stopScan();
635 mLocalMediaManager.unregisterCallback(mDeviceCallback);
636 }
Robert Snoeberger3cc22222020-03-25 15:36:31 -0400637 mIsRegistered = false;
638 }
639 }
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400640 /**
641 * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
642 * component to the list of resumption components
643 */
644 private void tryUpdateResumptionList(ComponentName componentName) {
645 Log.d(TAG, "Testing if we can connect to " + componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400646 if (mQSMediaBrowser != null) {
647 mQSMediaBrowser.disconnect();
648 }
649 mQSMediaBrowser = new QSMediaBrowser(mContext,
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400650 new QSMediaBrowser.Callback() {
651 @Override
652 public void onConnected() {
653 Log.d(TAG, "yes we can resume with " + componentName);
654 mServiceComponent = componentName;
655 updateResumptionList(componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400656 mQSMediaBrowser.disconnect();
657 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400658 }
659
660 @Override
661 public void onError() {
662 Log.d(TAG, "Cannot resume with " + componentName);
663 mServiceComponent = null;
Beth Thibodeau89f5c762020-04-21 13:09:55 -0400664 if (!hasMediaSession()) {
665 // If it's not active and we can't resume, remove
666 removePlayer();
667 }
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400668 mQSMediaBrowser.disconnect();
669 mQSMediaBrowser = null;
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400670 }
671 },
672 componentName);
Beth Thibodeauee42e8c2020-04-30 01:09:29 -0400673 mQSMediaBrowser.testConnection();
Beth Thibodeau23a33ab2020-04-07 20:51:57 -0400674 }
675
676 /**
677 * Add the component to the saved list of media browser services, checking for duplicates and
678 * removing older components that exceed the maximum limit
679 * @param componentName
680 */
681 private synchronized void updateResumptionList(ComponentName componentName) {
682 // Add to front of saved list
683 if (mSharedPrefs == null) {
684 mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
685 }
686 String componentString = componentName.flattenToString();
687 String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
688 if (listString == null) {
689 listString = componentString;
690 } else {
691 String[] components = listString.split(QSMediaBrowser.DELIMITER);
692 StringBuilder updated = new StringBuilder(componentString);
693 int nBrowsers = 1;
694 for (int i = 0; i < components.length
695 && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
696 if (componentString.equals(components[i])) {
697 continue;
698 }
699 updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
700 nBrowsers++;
701 }
702 listString = updated.toString();
703 }
704 mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
705 }
706
707 /**
708 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
709 */
710 protected void removePlayer() { }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500711}