blob: 2bf75f2ddb0b51b40f802d1cd29a393e88662eca [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media;
import static android.app.Notification.safeCharSequence;
import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.util.Log;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.settingslib.Utils;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import dagger.Lazy;
/**
* A view controller used for Media Playback.
*/
public class MediaControlPanel {
private static final String TAG = "MediaControlPanel";
private static final float DISABLED_ALPHA = 0.38f;
private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
// Button IDs for QS controls
static final int[] ACTION_IDS = {
R.id.action0,
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4
};
private final SeekBarViewModel mSeekBarViewModel;
private SeekBarObserver mSeekBarObserver;
protected final Executor mBackgroundExecutor;
private final ActivityStarter mActivityStarter;
private Context mContext;
private PlayerViewHolder mViewHolder;
private String mKey;
private MediaViewController mMediaViewController;
private MediaSession.Token mToken;
private MediaController mController;
private KeyguardDismissUtil mKeyguardDismissUtil;
private Lazy<MediaDataManager> mMediaDataManagerLazy;
private int mBackgroundColor;
private int mAlbumArtSize;
private int mAlbumArtRadius;
// This will provide the corners for the album art.
private final ViewOutlineProvider mViewOutlineProvider;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
/**
* Initialize a new control panel
* @param context
* @param backgroundExecutor background executor, used for processing artwork
* @param activityStarter activity starter
*/
@Inject
public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaViewController mediaViewController,
SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
mediaOutputDialogFactory) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
mSeekBarViewModel = seekBarViewModel;
mMediaViewController = mediaViewController;
mMediaDataManagerLazy = lazyMediaDataManager;
mKeyguardDismissUtil = keyguardDismissUtil;
mMediaOutputDialogFactory = mediaOutputDialogFactory;
loadDimens();
mViewOutlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, mAlbumArtSize, mAlbumArtSize, mAlbumArtRadius);
}
};
}
public void onDestroy() {
if (mSeekBarObserver != null) {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
}
mSeekBarViewModel.onDestroy();
mMediaViewController.onDestroy();
}
private void loadDimens() {
mAlbumArtRadius = mContext.getResources().getDimensionPixelSize(
Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
}
/**
* Get the view holder used to display media controls
* @return the view holder
*/
@Nullable
public PlayerViewHolder getView() {
return mViewHolder;
}
/**
* Get the view controller used to display media controls
* @return the media view controller
*/
@NonNull
public MediaViewController getMediaViewController() {
return mMediaViewController;
}
/**
* Sets the listening state of the player.
*
* Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
* unnecessary work when the QS panel is closed.
*
* @param listening True when player should be active. Otherwise, false.
*/
public void setListening(boolean listening) {
mSeekBarViewModel.setListening(listening);
}
/**
* Get the context
* @return context
*/
public Context getContext() {
return mContext;
}
/** Attaches the player to the view holder. */
public void attach(PlayerViewHolder vh) {
mViewHolder = vh;
TransitionLayout player = vh.getPlayer();
ImageView albumView = vh.getAlbumView();
albumView.setOutlineProvider(mViewOutlineProvider);
albumView.setClipToOutline(true);
mSeekBarObserver = new SeekBarObserver(vh);
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
mMediaViewController.attach(player);
mViewHolder.getPlayer().setOnLongClickListener(v -> {
if (!mMediaViewController.isGutsVisible()) {
mMediaViewController.openGuts();
return true;
} else {
return false;
}
});
mViewHolder.getCancel().setOnClickListener(v -> {
closeGuts();
});
mViewHolder.getSettings().setOnClickListener(v -> {
mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
});
}
/**
* Bind this view based on the data given
*/
public void bind(@NonNull MediaData data, String key) {
if (mViewHolder == null) {
return;
}
mKey = key;
MediaSession.Token token = data.getToken();
mBackgroundColor = data.getBackgroundColor();
if (mToken == null || !mToken.equals(token)) {
mToken = token;
}
if (mToken != null) {
mController = new MediaController(mContext, mToken);
} else {
mController = null;
}
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
mViewHolder.getPlayer().setBackgroundTintList(
ColorStateList.valueOf(mBackgroundColor));
// Click action
PendingIntent clickIntent = data.getClickIntent();
if (clickIntent != null) {
mViewHolder.getPlayer().setOnClickListener(v -> {
if (mMediaViewController.isGutsVisible()) return;
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
});
}
ImageView albumView = mViewHolder.getAlbumView();
boolean hasArtwork = data.getArtwork() != null;
if (hasArtwork) {
Drawable artwork = scaleDrawable(data.getArtwork());
albumView.setImageDrawable(artwork);
}
setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
// App icon
ImageView appIcon = mViewHolder.getAppIcon();
if (data.getAppIcon() != null) {
appIcon.setImageDrawable(data.getAppIcon());
} else {
Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
appIcon.setImageDrawable(iconDrawable);
}
// Song name
TextView titleText = mViewHolder.getTitleText();
titleText.setText(safeCharSequence(data.getSong()));
// App title
TextView appName = mViewHolder.getAppName();
appName.setText(data.getApp());
// Artist name
TextView artistText = mViewHolder.getArtistText();
artistText.setText(safeCharSequence(data.getArtist()));
// Transfer chip
mViewHolder.getSeamless().setVisibility(View.VISIBLE);
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
mViewHolder.getSeamless().setOnClickListener(v -> {
mMediaOutputDialogFactory.create(data.getPackageName(), true);
});
ImageView iconView = mViewHolder.getSeamlessIcon();
TextView deviceName = mViewHolder.getSeamlessText();
final MediaDeviceData device = data.getDevice();
final int seamlessId = mViewHolder.getSeamless().getId();
final int seamlessFallbackId = mViewHolder.getSeamlessFallback().getId();
final boolean showFallback = device != null && !device.getEnabled();
final int seamlessFallbackVisibility = showFallback ? View.VISIBLE : View.GONE;
mViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility);
expandedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
collapsedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
final int seamlessVisibility = showFallback ? View.GONE : View.VISIBLE;
mViewHolder.getSeamless().setVisibility(seamlessVisibility);
expandedSet.setVisibility(seamlessId, seamlessVisibility);
collapsedSet.setVisibility(seamlessId, seamlessVisibility);
final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f;
expandedSet.setAlpha(seamlessId, seamlessAlpha);
collapsedSet.setAlpha(seamlessId, seamlessAlpha);
// Disable clicking on output switcher for resumption controls.
mViewHolder.getSeamless().setEnabled(!data.getResumption());
if (showFallback) {
iconView.setImageDrawable(null);
deviceName.setText(null);
} else if (device != null) {
Drawable icon = device.getIcon();
iconView.setVisibility(View.VISIBLE);
if (icon instanceof AdaptiveIcon) {
AdaptiveIcon aIcon = (AdaptiveIcon) icon;
aIcon.setBackgroundColor(mBackgroundColor);
iconView.setImageDrawable(aIcon);
} else {
iconView.setImageDrawable(icon);
}
deviceName.setText(device.getName());
} else {
// Reset to default
Log.w(TAG, "device is null. Not binding output chip.");
iconView.setVisibility(View.GONE);
deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
}
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
// Media controls
int i = 0;
List<MediaAction> actionIcons = data.getActions();
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
int actionId = ACTION_IDS[i];
final ImageButton button = mViewHolder.getAction(actionId);
MediaAction mediaAction = actionIcons.get(i);
button.setImageDrawable(mediaAction.getDrawable());
button.setContentDescription(mediaAction.getContentDescription());
Runnable action = mediaAction.getAction();
if (action == null) {
button.setEnabled(false);
} else {
button.setEnabled(true);
button.setOnClickListener(v -> {
action.run();
});
}
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
}
// Hide any unused buttons
for (; i < ACTION_IDS.length; i++) {
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
}
// Seek Bar
final MediaController controller = getController();
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
// Guts label
boolean isDismissible = data.isClearable();
mViewHolder.getSettingsText().setText(isDismissible
? R.string.controls_media_close_session
: R.string.controls_media_active_session);
// Dismiss
mViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
mViewHolder.getDismiss().setEnabled(isDismissible);
mViewHolder.getDismiss().setOnClickListener(v -> {
if (mKey != null) {
closeGuts();
mKeyguardDismissUtil.executeWhenUnlocked(() -> {
mMediaDataManagerLazy.get().dismissMediaData(mKey,
MediaViewController.GUTS_ANIMATION_DURATION + 100);
return true;
}, /* requiresShadeOpen */ true);
} else {
Log.w(TAG, "Dismiss media with null notification. Token uid="
+ data.getToken().getUid());
}
});
// TODO: We don't need to refresh this state constantly, only if the state actually changed
// to something which might impact the measurement
mMediaViewController.refreshState();
}
/**
* Close the guts for this player.
* @param immediate {@code true} if it should be closed without animation
*/
public void closeGuts(boolean immediate) {
mMediaViewController.closeGuts(immediate);
}
private void closeGuts() {
closeGuts(false);
}
@UiThread
private Drawable scaleDrawable(Icon icon) {
if (icon == null) {
return null;
}
// Let's scale down the View, such that the content always nicely fills the view.
// ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
// ratios
Drawable drawable = icon.loadDrawable(mContext);
float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
Rect bounds;
if (aspectRatio > 1.0f) {
bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
} else {
bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
}
if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
bounds.offset((int) -offsetX,(int) -offsetY);
}
drawable.setBounds(bounds);
return drawable;
}
/**
* Get the current media controller
* @return the controller
*/
public MediaController getController() {
return mController;
}
/**
* Check whether the media controlled by this player is currently playing
* @return whether it is playing, or false if no controller information
*/
public boolean isPlaying() {
return isPlaying(mController);
}
/**
* Check whether the given controller is currently playing
* @param controller media controller to check
* @return whether it is playing, or false if no controller information
*/
protected boolean isPlaying(MediaController controller) {
if (controller == null) {
return false;
}
PlaybackState state = controller.getPlaybackState();
if (state == null) {
return false;
}
return (state.getState() == PlaybackState.STATE_PLAYING);
}
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
}
}