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