package me.barrasso.android.volume.popup;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.google.android.apps.dashclock.configuration.ColorPreference;
import com.squareup.otto.Subscribe;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import me.barrasso.android.volume.R;
import me.barrasso.android.volume.VolumeAccessibilityService;
import me.barrasso.android.volume.media.StreamResources;
import me.barrasso.android.volume.media.VolumePanelInfo;
import me.barrasso.android.volume.media.compat.RemoteControlCompat;
import me.barrasso.android.volume.media.conditions.RingerNotificationLink;
import me.barrasso.android.volume.ui.transition.TransitionCompat;
import me.barrasso.android.volume.utils.AudioHelper;
import me.barrasso.android.volume.utils.Utils;
import me.barrasso.android.volume.utils.VolumeManager;
import static me.barrasso.android.volume.LogUtils.LOGD;
import static me.barrasso.android.volume.LogUtils.LOGE;
import static me.barrasso.android.volume.LogUtils.LOGI;
import static me.barrasso.android.volume.LogUtils.LOGW;
/**
* Referred to as "Drop" in the app.<br />
* An original theme, like {@link me.barrasso.android.volume.popup.HeadsUpVolumePanel}. It's an
* awesome theme because it includes BOTH multiple-channel support (like {@link me.barrasso.android.volume.popup.ParanoidVolumePanel}
* AND contextual music controls (only while music is playing). It's the most useful theme yet!
*/
public class UberVolumePanel extends VolumePanel implements AdapterView.OnItemSelectedListener {
public static final String TAG = UberVolumePanel.class.getSimpleName();
public static final VolumePanelInfo<UberVolumePanel> VOLUME_PANEL_INFO =
new VolumePanelInfo<UberVolumePanel>(UberVolumePanel.class);
public UberVolumePanel(PopupWindowManager pWindowManager) {
super(pWindowManager);
}
private TransitionCompat transition;
private boolean hasAlbumArt;
View divider;
ImageView album;
TextView artist;
TextView song;
Spinner spinner;
ProgressBar seekBar;
ViewGroup root, musicPanel, sliderGroup, visiblePanel;
ImageButton playPause, mBtnNext;
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate() {
super.onCreate();
Context context = getContext();
transition = TransitionCompat.get();
boolean darkColor = ColorPreference.isColorDark(color);
int theme = (darkColor) ? android.R.style.Theme_Holo_Light : android.R.style.Theme_Holo;
Context themeContext = new ContextThemeWrapper(context, theme);
context.getApplicationContext().setTheme(theme);
LayoutInflater inflater = LayoutInflater.from(themeContext);
FrameLayout parent = new FrameLayout(themeContext);
root = (ViewGroup) inflater.inflate(R.layout.uber_volume_adjust, parent, false);
context.getApplicationContext().setTheme(R.style.AppTheme);
visiblePanel = (ViewGroup) root.findViewById(R.id.visible_panel);
seekBar = (ProgressBar) root.findViewById(android.R.id.progress);
spinner = (Spinner) root.findViewById(R.id.stream_icon);
album = (ImageView) root.findViewById(R.id.album_art);
artist = (TextView) root.findViewById(R.id.track_artist);
song = (TextView) root.findViewById(R.id.track_song);
musicPanel = (ViewGroup) root.findViewById(R.id.music_panel);
divider = root.findViewById(R.id.divider);
playPause = (ImageButton) root.findViewById(R.id.media_play_pause);
mBtnNext = (ImageButton) root.findViewById(R.id.media_next);
album.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openMusic();
}
});
LayerDrawable layer = (LayerDrawable) seekBar.getProgressDrawable();
layer.findDrawableByLayerId(android.R.id.progress).mutate()
.setColorFilter(HeadsUpVolumePanel._COLOR, PorterDuff.Mode.MULTIPLY);
attachPlaybackListeners(root, new MediaButtonClickListener());
toggleSeekBar(seek);
setEnableMarquee(true);
initSpinner();
updateMediaIcons();
transition.beginDelayedTransition((ViewGroup) root.findViewById(R.id.slider_group));
mLayout = root;
}
@Override
public void setBackgroundColor(final int newColor) {
super.setBackgroundColor(newColor);
visiblePanel.setBackgroundColor(newColor);
spinner.setPopupBackgroundDrawable(new ColorDrawable(backgroundColor));
spinner.invalidate();
}
@Override
public void setColor(final int newColor) {
super.setColor(newColor);
toggleSeekBar(seek);
updateMediaIcons();
StreamAdapter adapter = ((StreamAdapter) spinner.getAdapter());
adapter.setColor(newColor);
adapter.notifyDataSetChanged();
spinner.invalidate();
}
@Override
public void setTertiaryColor(final int newColor) {
super.setTertiaryColor(newColor);
song.setTextColor(newColor);
artist.setTextColor(newColor);
}
@Override
protected void adjustVolume(int direction) {
LOGI(TAG, "adjustVolume(" + VolumeManager.getStreamName(mCurrentStream) + ", " + direction + ')');
if (mCurrentStream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
super.adjustVolume(direction);
} else {
lastDirection = direction; // Needed because we don't call super
adjustStreamVolume(direction, mCurrentStream);
}
}
private int mCurrentStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
@Override
public void onItemSelected(AdapterView<?> parent, View view,
int pos, long id) {
StreamAdapter adapter = ((StreamAdapter) spinner.getAdapter());
LOGI(TAG, "onItemSelected(" + pos + ", " + id + ')');
StreamResources resources = StreamResources.resourceForStreamType(mStreamIndices.keyAt(pos));
seekBar.setMax(getStreamMaxVolume(resources.getStreamType()));
seekBar.setProgress(getStreamVolume(resources.getStreamType()));
seekBar.setTag(resources);
mCurrentStream = resources.getStreamType();
adapter.notifyDataSetChanged();
spinner.invalidate();
}
@Override public void onNothingSelected(AdapterView<?> parent) { }
protected void loadSystemSettings() {
// For now, only show master volume if master volume is supported.
if (null == mAudioHelper) mAudioHelper = AudioHelper.getHelper(getContext(), null);
boolean useMasterVolume = mAudioHelper.useMasterVolume();
if (useMasterVolume) {
for (int i = 0; i < StreamResources.STREAMS.length; i++) {
StreamResources streamRes = StreamResources.STREAMS[i];
streamRes.show(streamRes.getStreamType() == STREAM_MASTER);
}
}
}
private SparseIntArray mStreamIndices;
protected void initSpinner() {
if (null == mAudioManager) mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
loadSystemSettings();
boolean mVoiceCapable = mAudioHelper.isVoiceCapable();
List<StreamResources> streams = new ArrayList<StreamResources>();
RingerNotificationLink linkChecker = new RingerNotificationLink();
mNotificationRingerLink = linkChecker.apply(mAudioManager);
for (StreamResources stream : StreamResources.STREAMS) {
final int streamType = stream.getStreamType();
if (!stream.show()) {
continue;
}
// Skip ring volume for non-phone devices
if (!mVoiceCapable && streamType == AudioManager.STREAM_RING) {
continue;
}
// Skip notification volume if linked with ring volume
if (streamType == AudioManager.STREAM_NOTIFICATION) {
if (mVoiceCapable && mNotificationRingerLink) {
continue;
} else if (linkNotifRinger) {
// User has asked to link notification & ringer volume.
continue;
}
}
streams.add(stream);
}
int i = 0;
mStreamIndices = new SparseIntArray(streams.size());
for (StreamResources stream : streams) {
mStreamIndices.put(stream.getStreamType(), i);
stream.setVolume(getStreamVolume(stream.getStreamType()));
++i;
}
StreamAdapter adapter = new StreamAdapter(getContext(), 0, streams);
adapter.setDropDownViewResource(R.layout.spinner_icon);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
spinner.setOnTouchListener(new View.OnTouchListener() {
@Override public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP)
onUserInteraction();
return view.onTouchEvent(motionEvent);
}
});
adapter.setWidth(Math.min(spinner.getWidth(), spinner.getHeight()));
}
protected String mCurrentPackage;
@Override
public void show() {
// Don't show the music panel if we're in the music app.
if (!TextUtils.isEmpty(musicPackageName) && !TextUtils.isEmpty(mCurrentPackage)) {
if (mCurrentPackage.equals(musicPackageName)) {
setMusicVisibility(View.GONE);
} else if (mMusicActive) {
setMusicVisibility(View.VISIBLE);
}
}
super.show();
}
@Override
public void onTopAppChanged(VolumeAccessibilityService.TopApp app) {
super.onTopAppChanged(app);
mCurrentPackage = app.mCurrentPackage;
}
@Override public void setSeek(final boolean shouldSeek) {
super.setSeek(shouldSeek);
toggleSeekBar(shouldSeek);
}
@Override
public void screen(boolean on) {
super.screen(on);
setEnableMarquee(on);
}
@Override public void setOneVolume(boolean one) { /* No-op */ }
private void setEnableMarquee(boolean enabled) {
LOGD(TAG, "setEnableMarquee(" + enabled + ')');
if (artist != null) artist.setSelected(enabled);
if (song != null) song.setSelected(enabled);
}
protected void toggleSeekBar(final boolean shouldSeek) {
// If we've got a SeekBar, handle seeking!
if (seekBar instanceof SeekBar) {
SeekBar seeker = (SeekBar) seekBar;
seeker.setOnSeekBarChangeListener((shouldSeek) ? this : null);
seeker.setOnTouchListener((shouldSeek) ? null : noTouchListener);
Drawable thumb = null;
if (shouldSeek) {
thumb = getResources().getDrawable(R.drawable.scrubber_control_selector_mini);
thumb.mutate().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
thumb.setBounds(0,0, thumb.getIntrinsicWidth(), thumb.getIntrinsicHeight());
}
seeker.setThumb(thumb);
// NOTE: there's so weird issue with setting the thumb dynamically.
// This seems to do the trick (fingers crossed).
Utils.tap((View) seeker.getParent());
seeker.invalidate();
}
}
// Bit of a misnomer because it'll launch settings when music isn't playing.
/*package*/ void openMusic() {
hide();
if (hasAlbumArt) {
launchMusicApp();
} else {
Intent volumeSettings = new Intent(Settings.ACTION_SOUND_SETTINGS);
startActivity(volumeSettings);
}
}
public void setMusicVisibility(int visibility) {
LOGI(TAG, "setMusicVisibility(" + visibility + ')');
divider.setVisibility(visibility);
musicPanel.setVisibility(visibility);
}
@Override
public void onStreamVolumeChange(int streamType, int volume, int max) {
// TODO: set the spinner to the stream matching the change.
// All changes to the SeekBar should be handled by this stream.
StreamResources resources = StreamResources.resourceForStreamType(streamType);
resources.setVolume(volume);
spinner.setSelection(mStreamIndices.get(streamType));
StreamAdapter adapter = ((StreamAdapter) spinner.getAdapter());
adapter.setColor(color);
adapter.notifyDataSetChanged();
spinner.invalidate();
LayerDrawable layer = (LayerDrawable) seekBar.getProgressDrawable();
layer.findDrawableByLayerId(android.R.id.progress).mutate().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
visiblePanel.setBackgroundColor(backgroundColor);
seekBar.setMax(max);
seekBar.setProgress(volume);
seekBar.setTag(resources);
show();
}
protected boolean hideMusicWithPanel;
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onPlayStateChanged(Pair<MediaMetadataCompat, PlaybackStateCompat> mediaInfo) {
if (!created) return;
super.onPlayStateChanged(mediaInfo);
LOGI(TAG, "onPlayStateChanged()");
LOGI(TAG, RemoteControlCompat.getMediaMetadataLog(mediaInfo.first));
String sTitle = mediaInfo.first.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
String sArtist = mediaInfo.first.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
boolean hasArtist = !TextUtils.isEmpty(sArtist);
boolean hasTitle = !TextUtils.isEmpty(sTitle);
boolean missingData = (!hasArtist || !hasTitle);
if (mMusicActive && !missingData) {
transition.beginDelayedTransition(musicPanel, TransitionCompat.KEY_AUDIO_TRANSITION);
}
// Update button visibility based on the transport flags.
if (null == mediaInfo.second || mediaInfo.second.getActions() <= 0) {
mBtnNext.setVisibility(View.VISIBLE);
playPause.setVisibility(View.VISIBLE);
} else {
final long flags = mediaInfo.second.getActions();
setVisibilityBasedOnFlag(mBtnNext, flags, PlaybackStateCompat.ACTION_SKIP_TO_NEXT);
setVisibilityBasedOnFlag(playPause, flags,
PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY_PAUSE);
}
song.setText(sTitle);
artist.setText(sArtist);
// If we have album art, use it!
if (mMusicActive) {
Bitmap albumArtBitmap = null;
try {
albumArtBitmap = RemoteControlCompat.getBitmap(getContext(), mediaInfo.first);
} catch (FileNotFoundException fne) {
LOGE(TAG, "Album art URI invalid.", fne);
}
if (null != albumArtBitmap) {
LOGI(TAG, "Loading artwork bitmap.");
album.setImageAlpha(0xFF);
album.setColorFilter(null);
album.setImageBitmap(albumArtBitmap);
hasAlbumArt = true;
} else {
hasAlbumArt = false;
}
}
// Next, we'll try to display the app's icon.
if (mMusicActive && !hasAlbumArt && !TextUtils.isEmpty(musicPackageName)) {
Drawable appIcon = null;
try {
Bitmap iconBmp = RemoteControlCompat.getBitmap(getContext(), mediaInfo.first);
if (null != iconBmp) {
appIcon = new BitmapDrawable(getContext().getResources(), iconBmp);
LOGI(TAG, "App icon loaded from MediaMetadata instead.");
}
} catch (FileNotFoundException fne) {
LOGE(TAG, "Album art URI invalid.", fne);
}
if (null == appIcon)
appIcon = getAppIcon(musicPackageName);
if (null != appIcon) {
LOGI(TAG, "Loading app icon instead of album art.");
final ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
album.setColorFilter(new ColorMatrixColorFilter(cm));
appIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
album.setImageAlpha(0xEF);
album.setImageDrawable(appIcon);
hasAlbumArt = true;
} else {
hasAlbumArt = false;
}
}
album.setVisibility((hasAlbumArt) ? View.VISIBLE : View.GONE);
if (!mMusicActive) {
hideMusicWithPanel = true;
}
updatePlayState();
if (mMusicActive) setMusicVisibility(View.VISIBLE);
}
protected void updateMediaIcons() {
LOGI(TAG, "updateMediaIcons()");
Resources res = getResources();
Drawable next = res.getDrawable(R.drawable.ic_media_next_white);
next.mutate().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
mBtnNext.setImageDrawable(next);
updatePlayState();
}
/*package*/ void updatePlayState() {
int icon = ((mMusicActive) ? R.drawable.ic_media_pause_white : R.drawable.ic_media_play_white);
Drawable play = getResources().getDrawable(icon);
play.mutate().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
playPause.setImageDrawable(play);
}
protected Drawable getAppIcon(String packageName) {
PackageManager mPM = getContext().getPackageManager();
try {
return mPM.getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException nfe) {
LOGE(TAG, "Couldn't get app icon for `" + packageName + "`", nfe);
return null;
}
}
@Override public void onVisibilityChanged(int visibility) {
LOGI(TAG, "onVisibilityChanged(" + visibility + ')');
super.onVisibilityChanged(visibility);
switch (visibility) {
case View.GONE:
if (hideMusicWithPanel) {
hideMusicWithPanel = false;
setMusicVisibility(View.GONE);
}
setEnableMarquee(false);
mCurrentStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
break;
case View.VISIBLE:
setEnableMarquee(true);
break;
}
}
@SuppressWarnings("unused")
@Subscribe
public void onPlaybackEvent(Pair<MediaMetadataCompat, PlaybackStateCompat> mediaInfo) {
// NOTE: This MUST be added to the final descendant of VolumePanel!
LOGI(TAG, "onPlaybackEvent()");
this.onPlayStateChanged(mediaInfo);
}
@Override public boolean isInteractive() { return true; }
@Override public boolean supportsMediaPlayback() { return true; }
@Override public void onRotationChanged(final int rotation) {
super.onRotationChanged(rotation);
mWindowAttributes = getWindowLayoutParams();
onWindowAttributesChanged();
}
@Override public void setStretch(boolean stretchy) {
super.setStretch(stretchy);
mWindowAttributes = getWindowLayoutParams();
onWindowAttributesChanged();
}
@Override
public WindowManager.LayoutParams getWindowLayoutParams() {
int flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED );
WindowManager.LayoutParams WPARAMS = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT,
0, 0, WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, flags, PixelFormat.TRANSLUCENT);
WPARAMS.windowAnimations = android.R.style.Animation_Translucent;
WPARAMS.packageName = getContext().getPackageName();
WPARAMS.setTitle(TAG);
WPARAMS.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
Resources res = getResources();
final int panelWidth = getNotificationPanelWidth();
final int maxWidth = ((panelWidth > 0) ? panelWidth : res.getDimensionPixelSize(R.dimen.notification_panel_width));
final int menuWidth = res.getDimensionPixelSize(R.dimen.max_menu_width);
final int screenWidth = getWindowWidth();
if (stretch || (maxWidth <= 0 && (!res.getBoolean(R.bool.isTablet) && screenWidth < menuWidth))) {
WPARAMS.gravity = (Gravity.FILL_HORIZONTAL | Gravity.TOP);
} else {
WPARAMS.gravity = (Gravity.CENTER_HORIZONTAL | Gravity.TOP);
WPARAMS.width = (maxWidth <= 0) ? menuWidth : maxWidth;
}
WPARAMS.screenBrightness = WPARAMS.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
return WPARAMS;
}
// Adapter for dealing with stream icons.
static class StreamAdapter extends ArrayAdapter<StreamResources> {
private int color = HeadsUpVolumePanel._COLOR;
private final LayoutInflater inflater;
private int size = 0;
public StreamAdapter(Context context, int layout, List<StreamResources> items) {
super(context, layout, items);
inflater = LayoutInflater.from(context);
}
public void setColor(final int colour) {
color = colour;
}
public void setWidth(final int newSize) { size = newSize; }
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
LOGI(TAG, "getDropDownView(" + position + ")");
return getView(position, convertView, parent);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView icon;
if (null == convertView) {
View layout = inflater.inflate(android.R.layout.simple_spinner_item, parent, false);
icon = (TextView) layout.findViewById(android.R.id.text1);
} else {
icon = (TextView) convertView.findViewById(android.R.id.text1);
}
// Update the icon accordingly. It's awkward, but if we use a custom layout then shit looks weird.
StreamResources resources = getItem(position);
LOGI(TAG, "getView(" + position + "), stream = " + VolumeManager.getStreamName(resources.getStreamType()));
int iconRes = (resources.getVolume() <= 0 ? resources.getIconMuteRes() : resources.getIconRes());
Drawable drawable = getContext().getResources().getDrawable(iconRes);
drawable.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
final int defIconSize = Resources.getSystem().getDimensionPixelSize(android.R.dimen.app_icon_size);
int iconSize = defIconSize;
if (size > 0) {
iconSize = size;
} else {
if (position > 0) iconSize = ((iconSize * 3) / 4);
else iconSize = ((iconSize * 4) / 3);
}
drawable.setBounds(0, 0, iconSize, iconSize);
icon.setGravity(Gravity.CENTER);
icon.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
icon.setCompoundDrawablePadding(defIconSize / 5);
icon.setPadding((defIconSize / 6), (defIconSize / 5), (defIconSize / 6), (defIconSize / 5));
return icon;
}
}
}