blob: e208ee2b4ab77478dc2297cc3d88332a6c8aab84 [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
19import android.annotation.LayoutRes;
20import android.app.PendingIntent;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.content.res.ColorStateList;
27import android.graphics.Bitmap;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.GradientDrawable;
30import android.graphics.drawable.Icon;
31import android.graphics.drawable.RippleDrawable;
32import android.media.MediaMetadata;
33import android.media.session.MediaController;
34import android.media.session.MediaSession;
35import android.media.session.PlaybackState;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050036import android.util.Log;
37import android.view.KeyEvent;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.ImageButton;
42import android.widget.ImageView;
43import android.widget.LinearLayout;
44import android.widget.TextView;
45
46import androidx.core.graphics.drawable.RoundedBitmapDrawable;
47import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
48
49import com.android.settingslib.media.MediaDevice;
50import com.android.settingslib.media.MediaOutputSliceConstants;
51import com.android.settingslib.widget.AdaptiveIcon;
52import com.android.systemui.Dependency;
53import com.android.systemui.R;
54import com.android.systemui.plugins.ActivityStarter;
55import com.android.systemui.statusbar.NotificationMediaManager;
56
57import java.util.List;
58import java.util.concurrent.Executor;
59
60/**
61 * Base media control panel for System UI
62 */
63public class MediaControlPanel implements NotificationMediaManager.MediaListener {
64 private static final String TAG = "MediaControlPanel";
65 private final NotificationMediaManager mMediaManager;
Beth Thibodeaua51c3142020-03-17 17:27:04 -040066 private final Executor mForegroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -050067 private final Executor mBackgroundExecutor;
68
69 private Context mContext;
70 protected LinearLayout mMediaNotifView;
71 private View mSeamless;
72 private MediaSession.Token mToken;
73 private MediaController mController;
74 private int mForegroundColor;
75 private int mBackgroundColor;
76 protected ComponentName mRecvComponent;
77
78 private final int[] mActionIds;
79
80 // Button IDs used in notifications
81 protected static final int[] NOTIF_ACTION_IDS = {
82 com.android.internal.R.id.action0,
83 com.android.internal.R.id.action1,
84 com.android.internal.R.id.action2,
85 com.android.internal.R.id.action3,
86 com.android.internal.R.id.action4
87 };
88
89 private MediaController.Callback mSessionCallback = new MediaController.Callback() {
90 @Override
91 public void onSessionDestroyed() {
92 Log.d(TAG, "session destroyed");
93 mController.unregisterCallback(mSessionCallback);
94 clearControls();
95 }
96 };
97
98 /**
99 * Initialize a new control panel
100 * @param context
101 * @param parent
102 * @param manager
103 * @param layoutId layout resource to use for this control panel
104 * @param actionIds resource IDs for action buttons in the layout
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400105 * @param foregroundExecutor foreground executor
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500106 * @param backgroundExecutor background executor, used for processing artwork
107 */
108 public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager,
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400109 @LayoutRes int layoutId, int[] actionIds, Executor foregroundExecutor,
110 Executor backgroundExecutor) {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500111 mContext = context;
112 LayoutInflater inflater = LayoutInflater.from(mContext);
113 mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
114 mMediaManager = manager;
115 mActionIds = actionIds;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400116 mForegroundExecutor = foregroundExecutor;
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500117 mBackgroundExecutor = backgroundExecutor;
118 }
119
120 /**
121 * Get the view used to display media controls
122 * @return the view
123 */
124 public View getView() {
125 return mMediaNotifView;
126 }
127
128 /**
129 * Get the context
130 * @return context
131 */
132 public Context getContext() {
133 return mContext;
134 }
135
136 /**
137 * Update the media panel view for the given media session
138 * @param token
139 * @param icon
140 * @param iconColor
141 * @param bgColor
142 * @param contentIntent
143 * @param appNameString
144 * @param device
145 */
146 public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
147 int bgColor, PendingIntent contentIntent, String appNameString, MediaDevice device) {
148 mToken = token;
149 mForegroundColor = iconColor;
150 mBackgroundColor = bgColor;
151 mController = new MediaController(mContext, mToken);
152
153 MediaMetadata mediaMetadata = mController.getMetadata();
154
155 // Try to find a receiver for the media button that matches this app
156 PackageManager pm = mContext.getPackageManager();
157 Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
158 List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
159 if (info != null) {
160 for (ResolveInfo inf : info) {
161 if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
162 mRecvComponent = inf.getComponentInfo().getComponentName();
163 }
164 }
165 }
166
167 mController.registerCallback(mSessionCallback);
168
169 if (mediaMetadata == null) {
170 Log.e(TAG, "Media metadata was null");
171 return;
172 }
173
174 ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
175 if (albumView != null) {
176 // Resize art in a background thread
177 mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
178 }
179 mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
180
181 // Click action
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400182 if (contentIntent != null) {
183 mMediaNotifView.setOnClickListener(v -> {
184 try {
185 contentIntent.send();
186 // Also close shade
187 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
188 } catch (PendingIntent.CanceledException e) {
189 Log.e(TAG, "Pending intent was canceled", e);
190 }
191 });
192 }
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500193
194 // App icon
195 ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
196 Drawable iconDrawable = icon.loadDrawable(mContext);
197 iconDrawable.setTint(mForegroundColor);
198 appIcon.setImageDrawable(iconDrawable);
199
200 // Song name
201 TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
202 String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
203 titleText.setText(songName);
204 titleText.setTextColor(mForegroundColor);
205
206 // Not in mini player:
207 // App title
208 TextView appName = mMediaNotifView.findViewById(R.id.app_name);
209 if (appName != null) {
210 appName.setText(appNameString);
211 appName.setTextColor(mForegroundColor);
212 }
213
214 // Artist name
215 TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
216 if (artistText != null) {
217 String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
218 artistText.setText(artistName);
219 artistText.setTextColor(mForegroundColor);
220 }
221
222 // Transfer chip
223 mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
224 if (mSeamless != null) {
225 mSeamless.setVisibility(View.VISIBLE);
226 updateDevice(device);
227 ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
228 mSeamless.setOnClickListener(v -> {
229 final Intent intent = new Intent()
230 .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
231 .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
232 mController.getPackageName())
233 .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
234 mActivityStarter.startActivity(intent, false, true /* dismissShade */,
235 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
236 });
237 }
238
239 // Ensure is only added once
240 mMediaManager.removeCallback(this);
241 mMediaManager.addCallback(this);
242 }
243
244 /**
245 * Return the token for the current media session
246 * @return the token
247 */
248 public MediaSession.Token getMediaSessionToken() {
249 return mToken;
250 }
251
252 /**
253 * Get the current media controller
254 * @return the controller
255 */
256 public MediaController getController() {
257 return mController;
258 }
259
260 /**
261 * Get the name of the package associated with the current media controller
262 * @return the package name
263 */
264 public String getMediaPlayerPackage() {
265 return mController.getPackageName();
266 }
267
268 /**
269 * Check whether this player has an attached media session.
270 * @return whether there is a controller with a current media session.
271 */
272 public boolean hasMediaSession() {
273 return mController != null && mController.getPlaybackState() != null;
274 }
275
276 /**
277 * Check whether the media controlled by this player is currently playing
278 * @return whether it is playing, or false if no controller information
279 */
280 public boolean isPlaying() {
281 return isPlaying(mController);
282 }
283
284 /**
285 * Check whether the given controller is currently playing
286 * @param controller media controller to check
287 * @return whether it is playing, or false if no controller information
288 */
289 protected boolean isPlaying(MediaController controller) {
290 if (controller == null) {
291 return false;
292 }
293
294 PlaybackState state = controller.getPlaybackState();
295 if (state == null) {
296 return false;
297 }
298
299 return (state.getState() == PlaybackState.STATE_PLAYING);
300 }
301
302 /**
303 * Process album art for layout
304 * @param metadata media metadata
305 * @param albumView view to hold the album art
306 */
307 private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
308 Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
309 float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
310 RoundedBitmapDrawable roundedDrawable = null;
311 if (albumArt != null) {
312 Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
313 int albumSize = (int) mContext.getResources().getDimension(
314 R.dimen.qs_media_album_size);
315 Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
316 roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
317 roundedDrawable.setCornerRadius(radius);
318 } else {
319 Log.e(TAG, "No album art available");
320 }
321
322 // Now that it's resized, update the UI
323 final RoundedBitmapDrawable result = roundedDrawable;
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400324 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500325 if (result != null) {
326 albumView.setImageDrawable(result);
327 albumView.setVisibility(View.VISIBLE);
328 } else {
329 albumView.setImageDrawable(null);
330 albumView.setVisibility(View.GONE);
331 }
332 });
333 }
334
335 /**
336 * Update the current device information
337 * @param device device information to display
338 */
339 public void updateDevice(MediaDevice device) {
340 if (mSeamless == null) {
341 return;
342 }
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400343 mForegroundExecutor.execute(() -> {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500344 updateChipInternal(device);
345 });
346 }
347
348 private void updateChipInternal(MediaDevice device) {
349 ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
350
351 // Update the outline color
352 LinearLayout viewLayout = (LinearLayout) mSeamless;
353 RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
354 GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
355 rect.setStroke(2, mForegroundColor);
356 rect.setColor(mBackgroundColor);
357
358 ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
359 TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
360 deviceName.setTextColor(fgTintList);
361
362 if (device != null) {
363 Drawable icon = device.getIcon();
364 iconView.setVisibility(View.VISIBLE);
365 iconView.setImageTintList(fgTintList);
366
367 if (icon instanceof AdaptiveIcon) {
368 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
369 aIcon.setBackgroundColor(mBackgroundColor);
370 iconView.setImageDrawable(aIcon);
371 } else {
372 iconView.setImageDrawable(icon);
373 }
374 deviceName.setText(device.getName());
375 } else {
376 // Reset to default
377 iconView.setVisibility(View.GONE);
378 deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
379 }
380 }
381
382 /**
383 * Put controls into a resumption state
384 */
385 public void clearControls() {
386 // Hide all the old buttons
387 for (int i = 0; i < mActionIds.length; i++) {
388 ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
389 if (thisBtn != null) {
390 thisBtn.setVisibility(View.GONE);
391 }
392 }
393
394 // Add a restart button
395 ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
396 btn.setOnClickListener(v -> {
397 Log.d(TAG, "Attempting to restart session");
398 // Send a media button event to previously found receiver
399 if (mRecvComponent != null) {
400 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
401 intent.setComponent(mRecvComponent);
402 int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
403 intent.putExtra(
404 Intent.EXTRA_KEY_EVENT,
405 new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
406 mContext.sendBroadcast(intent);
407 } else {
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500408 // If we don't have a receiver, try relaunching the activity instead
Beth Thibodeaua51c3142020-03-17 17:27:04 -0400409 if (mController.getSessionActivity() != null) {
410 try {
411 mController.getSessionActivity().send();
412 } catch (PendingIntent.CanceledException e) {
413 Log.e(TAG, "Pending intent was canceled", e);
414 }
415 } else {
416 Log.e(TAG, "No receiver or activity to restart");
Beth Thibodeau7b6c1782020-03-05 11:43:51 -0500417 }
418 }
419 });
420 btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
421 btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
422 btn.setVisibility(View.VISIBLE);
423 }
424
425 @Override
426 public void onMetadataOrStateChanged(MediaMetadata metadata, int state) {
427 if (state == PlaybackState.STATE_NONE) {
428 clearControls();
429 mMediaManager.removeCallback(this);
430 }
431 }
432}