package me.barrasso.android.volume.popup; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.media.AudioManager; import android.os.Build; import android.provider.Settings; import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageView; import android.widget.SeekBar; import me.barrasso.android.volume.R; import me.barrasso.android.volume.media.StreamResources; import me.barrasso.android.volume.media.VolumePanelInfo; import me.barrasso.android.volume.ui.Expandable; import me.barrasso.android.volume.ui.MaxWidthLinearLayout; import me.barrasso.android.volume.utils.AudioHelper; import me.barrasso.android.volume.utils.Constants; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static me.barrasso.android.volume.LogUtils.LOGD; import static me.barrasso.android.volume.LogUtils.LOGI; /** * Theme based off Paranoid Android's (4.4) volume panel, with the current * slider at the top and an option to expand and control other streams. */ public class ParanoidVolumePanel extends VolumePanel implements Expandable { public static final String TAG = ParanoidVolumePanel.class.getSimpleName(); public static final VolumePanelInfo<ParanoidVolumePanel> VOLUME_PANEL_INFO = new VolumePanelInfo<ParanoidVolumePanel>(ParanoidVolumePanel.class); public ParanoidVolumePanel(PopupWindowManager pWindowManager) { super(pWindowManager); } /** The visible portion of the volume overlay */ protected ViewGroup mPanel; /** Contains the sliders and their touchable icons */ protected ViewGroup mSliderGroup; /** The button that expands the dialog to show all sliders */ protected View mMoreButton; protected ViewGroup root; /** Currently active stream that shows up at the top of the list of sliders */ protected int mActiveStreamType = -1; protected boolean mVolumeLinkNotification = false; protected void parentOnCreate() { super.onCreate(); } @SuppressWarnings("deprecation") @Override public void onCreate() { super.onCreate(); oneVolume = false; Context context = getContext(); LayoutInflater inflater = LayoutInflater.from(context); // Load default PA color if the user doesn't have a preference. if (!has(COLOR)) color = context.getResources().getColor(R.color.pa_volume_scrubber); if (!has(BACKGROUND)) backgroundColor = context.getResources().getColor(R.color.volume_panel_bg); root = (ViewGroup) inflater.inflate(R.layout.pa_volume_adjust, null); mPanel = (ViewGroup) root.findViewById(R.id.visible_panel); mSliderGroup = (ViewGroup) root.findViewById(R.id.slider_group); mMoreButton = root.findViewById(R.id.expand_button); mPanel.setBackgroundColor(backgroundColor); loadSystemSettings(); mMoreButton.setOnClickListener(expandListener); if (null == mStreamControls) mStreamControls = new SparseArray<StreamControl>(StreamResources.STREAMS.length); mLayout = root; } protected void updateSize() { LOGI(TAG, "updateSize(stretch=" + stretch + ')'); if (mPanel instanceof MaxWidthLinearLayout) { int panelWidth = 0; if (!stretch) { panelWidth = getNotificationPanelWidth(); if (panelWidth <= 0) panelWidth = getResources().getDimensionPixelSize(R.dimen.volume_panel_screen_width); } ((MaxWidthLinearLayout) mPanel).setMaxWidth(panelWidth); } onWindowAttributesChanged(); } protected void loadSystemSettings() { // Not supported on all devices, but worth a check. ContentResolver cr = getContext().getContentResolver(); final int notifLink = Settings.System.getInt(cr, Constants.VOLUME_LINK_NOTIFICATION, Integer.MIN_VALUE); final int notifUseRinger = Settings.System.getInt(cr, Constants.NOTIFICATIONS_USE_RING_VOLUME, Integer.MIN_VALUE); if (notifLink != Integer.MIN_VALUE) { mVolumeLinkNotification = (notifLink == 1); } else if (notifUseRinger != Integer.MIN_VALUE) { mVolumeLinkNotification = (notifUseRinger == 1); } else { mVolumeLinkNotification = mNotificationRingerLink; } // 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); } } } protected View.OnClickListener expandListener = new View.OnClickListener() { @Override public void onClick(View view) { LOGI(TAG, "onClick(" + view.getId() + ") expand = " + R.id.expand_button); switch (view.getId()) { case R.id.expand_button: toggle(); return; } // Another way is to check if the two are equal. if (view.equals(mMoreButton)) toggle(); } }; protected void toggle() { if (isExpanded()) collapse(); else expand(); onUserInteraction(); } @Override public void setBackgroundColor(int bcolor) { super.setBackgroundColor(bcolor); mPanel.setBackgroundColor(backgroundColor); } @Override public void setColor(int fcolor) { super.setColor(fcolor); updateStates(); } @Override public void setTertiaryColor(int fcolor) { super.setTertiaryColor(fcolor); updateStates(); } @Override public void onVisibilityChanged(int visibility) { super.onVisibilityChanged(visibility); switch (visibility) { case View.GONE: mActiveStreamType = -1; break; } } @Override public void setOneVolume(boolean one) { /* No-op */ } protected int getItemLayout() { return R.layout.pa_volume_adjust_item; } protected void createSliders() { LOGI(TAG, "createSliders()"); StreamResources[] STREAMS = StreamResources.STREAMS; Context mContext = getContext(); LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); Resources res = mContext.getResources(); for (int i = 0; i < STREAMS.length; i++) { StreamResources streamRes = STREAMS[i]; int streamType = streamRes.getStreamType(); StreamControl sc = new StreamControl(); sc.streamType = streamType; sc.group = (ViewGroup) inflater.inflate(getItemLayout(), null); sc.group.setTag(sc); sc.icon = (ImageView) sc.group.findViewById(R.id.stream_icon); sc.icon.setTag(sc); sc.icon.setContentDescription(res.getString(streamRes.getDescRes())); sc.iconRes = streamRes.getIconRes(); sc.iconMuteRes = streamRes.getIconMuteRes(); int[] icons = _getStreamIcons(sc); sc.icon.setImageResource(icons[0]); sc.icon.setOnClickListener(getStreamClickListener()); sc.seekbarView = (SeekBar) sc.group.findViewById(android.R.id.progress); int plusOne = (streamType == STREAM_BLUETOOTH_SCO || streamType == AudioManager.STREAM_VOICE_CALL) ? 1 : 0; setProgressColor(sc.seekbarView, color); sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); sc.seekbarView.setOnSeekBarChangeListener(volumeSeekListener); sc.seekbarView.setTag(sc); onCreateStream(sc); mStreamControls.put(streamType, sc); } } protected void onCreateStream(StreamControl sc) { } protected View.OnClickListener getStreamClickListener() { return new View.OnClickListener() { @Override public void onClick(View view) { if (view instanceof ImageView) { launchSoundSettings(); } } }; } protected void launchSoundSettings() { hide(); Intent volumeSettings = new Intent(Settings.ACTION_SOUND_SETTINGS); startActivity(volumeSettings); } protected void reorderSliders(final int activeStreamType) { LOGI(TAG, "reorderSliders()"); mSliderGroup.removeAllViews(); StreamControl active = mStreamControls.get(activeStreamType); if (active == null) { mActiveStreamType = -1; } else { mSliderGroup.addView(active.group); mActiveStreamType = activeStreamType; active.group.setVisibility(View.VISIBLE); updateSlider(active); } addOtherVolumes(); } private void addOtherVolumes() { LOGI(TAG, "addOtherVolumes()"); boolean mVoiceCapable = mAudioHelper.isVoiceCapable(); for (StreamResources stream : StreamResources.STREAMS) { // Skip the phone specific ones and the active one final int streamType = stream.getStreamType(); StreamControl sc = mStreamControls.get(streamType); if (!stream.show() || streamType == mActiveStreamType) { 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; } } mSliderGroup.addView(sc.group); updateSlider(sc); } } public void updateStates() { LOGI(TAG, "updateStates()"); final int count = mSliderGroup.getChildCount(); for (int i = 0; i < count; i++) { StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); updateSlider(sc); } } protected int[] _getStreamIcons(StreamControl sc) { int[] icons = getStreamIcons(sc); if (sc.streamType == AudioManager.STREAM_NOTIFICATION || sc.streamType == AudioManager.STREAM_RING) { switch (mRingerMode) { case AudioManager.RINGER_MODE_VIBRATE: icons[1] = getVibrateIcon(); break; case AudioManager.RINGER_MODE_SILENT: icons[1] = getSilentIcon(); break; } } return icons; } protected int[] getStreamIcons(StreamControl sc) { if (sc.streamType == AudioManager.STREAM_NOTIFICATION || sc.streamType == AudioManager.STREAM_RING) { switch (mRingerMode) { case AudioManager.RINGER_MODE_VIBRATE: return new int[] { sc.iconRes, getVibrateIcon() }; case AudioManager.RINGER_MODE_SILENT: return new int[] { sc.iconRes, getSilentIcon() }; } } return new int[] { sc.iconRes, sc.iconMuteRes }; // Default icons } /** Update the mute and progress state of a slider */ private void updateSlider(StreamControl sc) { final int volume = getStreamVolume(sc.streamType); LOGI(TAG, "updateSlider(" + sc.streamType + ", " + volume + ")"); sc.seekbarView.setProgress(volume); final boolean muted = (isMuted(sc.streamType) || volume <= 0); // Force reloading the image resource sc.icon.setImageDrawable(null); int[] icons = _getStreamIcons(sc); int icon = muted ? icons[1] : icons[0]; if (isParanoid()) { Drawable drawable = getResources().getDrawable(icon); drawable.mutate().setColorFilter(tertiaryColor, PorterDuff.Mode.MULTIPLY); sc.icon.setImageDrawable(drawable); } else { sc.icon.setImageResource(icon); } setProgressColor(sc.seekbarView, color); onUpdateSlider(sc.group); } protected void onUpdateSlider(ViewGroup sliderGroup) { } protected void setProgressColor(SeekBar seekbar, final int tcolor) { Drawable thumb = null; LOGI(TAG, "setProgressColor(" + color + ")"); LayerDrawable layer = (LayerDrawable) seekbar.getProgressDrawable(); layer.findDrawableByLayerId(android.R.id.progress).mutate().setColorFilter(color, PorterDuff.Mode.MULTIPLY); thumb = getResources().getDrawable(R.drawable.scrubber_control_selector_white); thumb.mutate().setColorFilter(tcolor, PorterDuff.Mode.MULTIPLY); thumb.setBounds(0, 0, thumb.getIntrinsicWidth(), thumb.getIntrinsicHeight()); seekbar.setThumb(thumb); // NOTE: The call to Utils.tap was removed because it causes an update // to the volume progress and creates strange behaviour. seekbar.invalidate(); } @Override public boolean isExpanded() { View child = mSliderGroup.getChildAt(1); return (null != child && child.getVisibility() == View.VISIBLE); } protected int getExpandedIcon() { return R.drawable.ic_find_previous_holo_dark; } protected int getCollapsedIcon() { return R.drawable.ic_find_next_holo_dark; } @Override public void expand() { LOGI(TAG, "expand()"); final int count = mSliderGroup.getChildCount(); for (int i = 0; i < count; i++) { View child = mSliderGroup.getChildAt(i); if (child.getVisibility() != View.VISIBLE) { child.setVisibility(View.VISIBLE); onUpdateSlider((ViewGroup) child); } } if (mMoreButton instanceof ImageView) ((ImageView) mMoreButton).setImageResource(getExpandedIcon()); } private void hideSlider(final int mActiveStreamType) { LOGI(TAG, "hideSlider(" + mActiveStreamType + ')'); final int count = mSliderGroup.getChildCount(); for (int i = 0; i < count; i++) { StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); if (mActiveStreamType == sc.streamType) { mSliderGroup.getChildAt(i).setVisibility(View.GONE); } } } @Override public void collapse() { LOGI(TAG, "collapse()"); final int count = mSliderGroup.getChildCount(); for (int i = 1; i < count; i++) { mSliderGroup.getChildAt(i).setVisibility(View.GONE); } if (mMoreButton instanceof ImageView) ((ImageView) mMoreButton).setImageResource(getCollapsedIcon()); } /*package*/ SeekBar.OnSeekBarChangeListener volumeSeekListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { final Object tag = seekBar.getTag(); if (fromUser && tag instanceof StreamControl) { StreamControl sc = (StreamControl) tag; if (getStreamVolume(sc.streamType) != progress) { LOGI(TAG, "setStreamVolume(" + sc.streamType + ", " + progress + ')'); setStreamVolume(sc.streamType, progress); } } onUserInteraction(); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { final Object tag = seekBar.getTag(); if (tag instanceof StreamControl) { StreamControl sc = (StreamControl) tag; // because remote volume updates are asynchronous, AudioService might have received // a new remote volume value since the finger adjusted the slider. So when the // progress of the slider isn't being tracked anymore, adjust the slider to the last // "published" remote volume value, so the UI reflects the actual volume. if (sc.streamType == STREAM_REMOTE_MUSIC) { seekBar.setProgress(getStreamVolume(STREAM_REMOTE_MUSIC)); } } } }; @Override protected void onVolumeChanged(int streamType, int index, int prevIndex, boolean ringer) { // Under NO circumstances can this theme handle master volume! It wouldn't // make sense and all sliders would compete with one another. oneVolume = false; // Before displaying, let's create and reorder the sliders. synchronized (this) { if (null != mMoreButton) mMoreButton.setOnClickListener(expandListener); if (mStreamControls == null || mStreamControls.size() <= 0) { createSliders(); } if (streamType != mActiveStreamType) { hideSlider(mActiveStreamType); reorderSliders(streamType); } } super.onVolumeChanged(streamType, index, prevIndex, ringer); } @Override public void onStreamVolumeChange(int streamType, int volume, int max) { LOGD(TAG, "onStreamVolumeChange(" + streamType + ", " + volume + ", " + max + ")"); StreamControl sc = mStreamControls.get(streamType); if (sc != null) { if (sc.seekbarView.getMax() != max) { sc.seekbarView.setMax(max); } sc.seekbarView.setProgress(volume); } if (alwaysExpanded) expand(); show(); } @Override public void hide() { super.hide(); if (isExpanded() && !alwaysExpanded) collapse(); } protected int getVibrateIcon() { return R.drawable.ic_audio_ring_notif_vibrate; } protected int getSilentIcon() { return R.drawable.ic_audio_phone_mute; } @Override public void onRingerModeChange(int ringerMode) { // Pulled from VolumePanel to also have the small vibration/ feedback. switch (ringerMode) { case RINGER_MODE_VIBRATE: if (isEnabled()) mAudioHelper.vibrate(VIBRATE_DURATION); break; } LOGI(TAG, "onRingerModeChange(" + ringerMode + ')'); if (null != mLastVolumeChange && (mLastVolumeChange.mStreamType == AudioManager.STREAM_NOTIFICATION || mLastVolumeChange.mStreamType == AudioManager.STREAM_RING)) { StreamControl sc = mStreamControls.get(mLastVolumeChange.mStreamType); if (null != sc) { switch (ringerMode) { case RINGER_MODE_VIBRATE: sc.iconMuteRes = getVibrateIcon(); break; case RINGER_MODE_SILENT: default: sc.iconMuteRes = getSilentIcon(); break; } } } if (isShowing()) updateStates(); } @Override public void setStretch(boolean stretchIt) { LOGI(TAG, "setStretch(" + stretchIt + ')'); super.setStretch(stretchIt); if (isParanoid()) updateSize(); } @Override public void onRotationChanged(int rotation) { super.onRotationChanged(rotation); if (isParanoid()) onWindowAttributesChanged(); } @Override public boolean isInteractive() { return true; } protected boolean isParanoid() { return getClass().equals(ParanoidVolumePanel.class); } @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, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0, WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, flags, PixelFormat.TRANSLUCENT); int anim = getInternalStyle("Animation_DropDownDown"); if (anim > 0) WPARAMS.windowAnimations = anim; WPARAMS.packageName = getContext().getPackageName(); WPARAMS.setTitle(getName()); WPARAMS.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; WPARAMS.gravity = (Gravity.FILL_HORIZONTAL | Gravity.TOP); WPARAMS.screenBrightness = WPARAMS.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; return WPARAMS; } }