package me.barrasso.android.volume.popup; import android.app.KeyguardManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.ComponentName; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Color; import android.media.AudioRoutesInfo; import android.media.MediaRouter; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.RemoteException; import android.provider.AlarmClock; import android.provider.MediaStore; import android.provider.Settings; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Pair; import android.util.Property; import android.util.SparseArray; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.view.MotionEvent; import android.view.View; import android.view.KeyEvent; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.text.TextUtils; import android.media.AudioManager; import android.widget.Toast; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URISyntaxException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.Locale; import java.util.Set; import com.android.systemui.qs.GlobalSetting; import com.squareup.otto.MainThreadBus; import static android.media.AudioManager.ADJUST_LOWER; import static android.media.AudioManager.ADJUST_RAISE; import static android.media.AudioManager.ADJUST_SAME; import static android.media.AudioManager.FLAG_PLAY_SOUND; import static android.media.AudioManager.FLAG_VIBRATE; import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_MUSIC; import static android.media.AudioManager.STREAM_NOTIFICATION; import static android.media.AudioManager.STREAM_RING; import static android.media.AudioManager.STREAM_VOICE_CALL; import static android.media.AudioManager.USE_DEFAULT_STREAM_TYPE; 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.media.VolumeMediaReceiver.*; import me.barrasso.android.volume.VolumeAccessibilityService; import me.barrasso.android.volume.activities.NoyzeApp; import me.barrasso.android.volume.media.MediaProviderDelegate; import me.barrasso.android.volume.media.VolumeMediaReceiver; import me.barrasso.android.volume.media.compat.RemoteControlCompat; import me.barrasso.android.volume.media.conditions.RingerNotificationLink; import me.barrasso.android.volume.media.conditions.SystemVolume; import me.barrasso.android.volume.utils.AppTypeMonitor; import me.barrasso.android.volume.utils.AudioHelper; import me.barrasso.android.volume.utils.Constants; import me.barrasso.android.volume.ui.OnTouchClickListener; import me.barrasso.android.volume.R; import me.barrasso.android.volume.utils.SettingsHelper; import me.barrasso.android.volume.media.StreamResources; import me.barrasso.android.volume.utils.Utils; import me.barrasso.android.volume.utils.VolumeManager; /** * Handle the volume up and down keys.<br /><br /> * Deals with AudioManager and Volume/ Media related events. Subclasses * should present the UI as they see fit. Comes with built-in support for * {@link SeekBar} widgets as well as many UI events.<br /><br /> * Details: VolumePanel accepts input from two areas: {@link KeyEvent}s sent * from an {@link android.accessibilityservice.AccessibilityService}/ input stream, and a {@link BroadcastReceiver} * to handle various events. These monitor changes in system volume from various * streams, and {@link AudioManager} is used to adjust system volume as well as * handle media-related events.<br /><br /> * * <strike>This code really should be moved elsewhere. * * Seriously, it really really should be moved elsewhere. This is used by * android.media.AudioService, which actually runs in the system process, to * show the volume dialog when the user changes the volume. What a mess.</strike> */ public abstract class VolumePanel extends PopupWindow implements View.OnKeyListener, OnSeekBarChangeListener { /** Object that contains data for each slider */ public static final class StreamControl { public int streamType; public ViewGroup group; public ImageView icon; public SeekBar seekbarView; public int iconRes; public int iconMuteRes; } static class VolumeChangeInfo { public final int mStreamType; public final int mFromVolume; public final int mToVolume; public final long mEventTime; public VolumeChangeInfo(int s, int i, int j) { mStreamType = s; mFromVolume = i; mToVolume = j; mEventTime = System.currentTimeMillis(); } } protected static final int VIBRATE_DURATION = 300; /** @return The value of "com.android.systemui.R$dimen#notification_panel_width", or 0 if inaccessible. */ protected static int getNotificationPanelWidth() { int id = getSystemUiDimen("notification_panel_width"); if (id <= 0) return 0; return Resources.getSystem().getDimensionPixelSize(id); } // Property of all VolumePanels is there auto-hide/ timeout, maybe music app. public static final Property<VolumePanel, String> MUSIC_APP = Property.of(VolumePanel.class, String.class, "musicUri"); public static final Property<VolumePanel, Boolean> MASTER_VOLUME = Property.of(VolumePanel.class, Boolean.class, "oneVolume"); public static final Property<VolumePanel, Integer> RINGER_MODE = Property.of(VolumePanel.class, Integer.class, "ringerMode"); public static final Property<VolumePanel, String> ACTION_LONG_PRESS_VOLUME_UP = Property.of(VolumePanel.class, String.class, "longPressActionUp"); public static final Property<VolumePanel, String> ACTION_LONG_PRESS_VOLUME_DOWN = Property.of(VolumePanel.class, String.class, "longPressActionDown"); public static final Property<VolumePanel, Boolean> HIDE_FULLSCREEN = Property.of(VolumePanel.class, Boolean.class, "hideFullscreen"); public static final Property<VolumePanel, Integer> DEFAULT_STREAM = Property.of(VolumePanel.class, Integer.class, "defaultStream"); public static final Property<VolumePanel, Boolean> LINK_NOTIF_RINGER = Property.of(VolumePanel.class, Boolean.class, "linkNotifRinger"); public static final Property<VolumePanel, Integer> COLOR = Property.of(VolumePanel.class, Integer.TYPE, "color"); public static final Property<VolumePanel, Integer> BACKGROUND = Property.of(VolumePanel.class, Integer.TYPE, "backgroundColor"); public static final Property<VolumePanel, Integer> TERTIARY = Property.of(VolumePanel.class, Integer.TYPE, "tertiaryColor"); public static final Property<VolumePanel, Boolean> SEEK = Property.of(VolumePanel.class, Boolean.class, "seek"); public static final Property<VolumePanel, Boolean> NO_LONG_PRESS = Property.of(VolumePanel.class, Boolean.class, "noLongPress"); public static final Property<VolumePanel, Boolean> HIDE_CAMERA = Property.of(VolumePanel.class, Boolean.class, "hideCamera"); public static final Property<VolumePanel, Boolean> STRETCH = Property.of(VolumePanel.class, Boolean.class, "stretch"); public static final Property<VolumePanel, Boolean> ALWAYS_EXPANDED = Property.of(VolumePanel.class, Boolean.class, "alwaysExpanded"); public static final Property<VolumePanel, Boolean> FIRST_REVEAL = Property.of(VolumePanel.class, Boolean.class, "firstReveal"); public static IntentFilter VOLUME_MUSIC_EVENTS() { final IntentFilter VOLUME_MUSIC_EVENTS = new IntentFilter(); VOLUME_MUSIC_EVENTS.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); VOLUME_MUSIC_EVENTS.addAction(Constants.VOLUME_CHANGED_ACTION); VOLUME_MUSIC_EVENTS.addAction(Constants.MASTER_MUTE_CHANGED_ACTION); VOLUME_MUSIC_EVENTS.addAction(Constants.MASTER_VOLUME_CHANGED_ACTION); VOLUME_MUSIC_EVENTS.addAction(Intent.ACTION_MEDIA_BUTTON); VOLUME_MUSIC_EVENTS.addAction(Intent.ACTION_USER_PRESENT); VOLUME_MUSIC_EVENTS.addAction(Constants.ACTION_VOLUMEPANEL_SHOWN); VOLUME_MUSIC_EVENTS.addAction(Constants.ACTION_VOLUMEPANEL_HIDDEN); VOLUME_MUSIC_EVENTS.addAction(Intent.ACTION_HEADSET_PLUG); VOLUME_MUSIC_EVENTS.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); return VOLUME_MUSIC_EVENTS; } // Pseudo stream type for master volume public static final int STREAM_MASTER = -100; // Pseudo stream type for remote volume is defined in AudioService.STREAM_REMOTE_MUSIC /* @hide The audio stream for phone calls when connected on bluetooth */ public static final int DEF_STREAM_BLUETOOTH_SCO = 6; /** A fake stream type to match the notion of remote media playback */ public final static int DEF_STREAM_REMOTE_MUSIC = -200; // Reflection-attempted stream constants with localized default values. public static final int STREAM_BLUETOOTH_SCO = AudioHelper.getAudioSystemFlag( "STREAM_BLUETOOTH_SCO", DEF_STREAM_BLUETOOTH_SCO); public static final int STREAM_REMOTE_MUSIC = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ? DEF_STREAM_REMOTE_MUSIC : AudioHelper.getAudioServiceFlag( "STREAM_REMOTE_MUSIC", DEF_STREAM_REMOTE_MUSIC)); // ========== ########## ========== // ========== ########## ========== protected final VolumeMediaHandler mHandler; protected VolumeMediaReceiver mVolumeMediaReceiver; protected CallStateListener mCallStateListener; protected GlobalSetting mPriorityModeObserver; protected NoyzeApp app; protected VolumeManager mVolumeManager; protected AudioManager mAudioManager; protected AudioHelper mAudioHelper; protected WindowManager.LayoutParams mWindowAttributes; protected TelephonyManager mTelephonyManager; /** All the slider controls mapped by stream type */ protected SparseArray<StreamControl> mStreamControls; // AudioManager volume/ media states protected boolean mVibrateWhenRinging; protected boolean mSpeakerphoneOn; protected boolean mMusicActive; protected boolean mRingIsSilent; protected boolean mVolumeDirty; // Needs updating? protected boolean seek; // Use ProgressBar as SeekBar protected int mRingerMode, mLastRingerMode; protected int mLongPressTimeout; protected int defaultStream = USE_DEFAULT_STREAM_TYPE; // No stream protected boolean fullscreen; protected boolean stretch; protected boolean mNotificationRingerLink; protected boolean linkNotifRinger; protected boolean alwaysExpanded; protected boolean hideCamera = true; protected boolean hideFullscreen; protected boolean oneVolume; protected boolean firstReveal; protected int ringerMode; protected int color = Color.WHITE; protected int backgroundColor = Color.BLACK; protected int tertiaryColor = Color.GRAY; protected int mCallState = TelephonyManager.CALL_STATE_IDLE; protected boolean noLongPress = true; protected VolumeChangeInfo mLastVolumeChange; // protected AudioFlingerProxy mAudioFlingerProxy; protected AppTypeMonitor mAppTypeMonitor; protected MediaProviderDelegate mMediaProviderDelegate; /*package*/ int lastStreamType = USE_DEFAULT_STREAM_TYPE; // Actions for long-pressing the volume up/ down buttons. protected String longPressActionUp; protected String longPressActionDown; public VolumePanel(PopupWindowManager manager) { super(manager); Context context = manager.getContext(); app = (NoyzeApp) context.getApplicationContext(); mStreamControls = new SparseArray<StreamControl>(StreamResources.STREAMS.length); mHandler = new VolumeMediaHandler(context.getMainLooper()); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mVolumeManager = new VolumeManager(mAudioManager); mAudioHelper = AudioHelper.getHelper(context, mAudioManager); // mAudioFlingerProxy = new AudioFlingerProxy(); mAudioHelper.setHandler(mHandler); mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); if (MediaProviderDelegate.IS_V18) mMediaProviderDelegate = MediaProviderDelegate.getDelegate(context); // Register here: be SURE that the handler is not null. if (null == mVolumeMediaReceiver) { mVolumeMediaReceiver = new VolumeMediaReceiver(mHandler); IntentFilter events = Utils.merge(VOLUME_MUSIC_EVENTS(), Constants.MEDIA_ACTION_FILTER()); context.registerReceiver(mVolumeMediaReceiver, events); mVolumeMediaReceiver.setHandler(mHandler); } // Register for events related to the call state. if (null == mCallStateListener) { mCallState = mTelephonyManager.getCallState(); mCallStateListener = new CallStateListener(mHandler); mTelephonyManager.listen(mCallStateListener, PhoneStateListener.LISTEN_CALL_STATE); } initState(); } /** * Call {@link super#onCreate} AFTER initializing all necessary * {@link View}s. Here is where all event listeners are attached. */ @Override protected void onCreate() { hookIntoEvents(); // By default, let's update cancel values. setCancelable(true); setCanceledOnTouchOutside(true); setCloseOnLongClick(false); } // Localized from android.view.VolumePanel // Note: Master volume isn't handled for most methods to reduce complexity. // AudioManager checks if Master Volume is enabled, and handles these cases // for us through hidden methods. protected boolean isMuted(int streamType) { if (streamType == STREAM_MASTER) { Boolean ret = mAudioHelper.boolMethod("isMasterMute", null); if (null != ret) return ret; return false; } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) && (streamType == STREAM_REMOTE_MUSIC)) { Integer ret = mAudioHelper.intServiceMethod("getRemoteStreamVolume", null); if (null != ret) return (ret <= 0); return false; } else { Boolean ret = mAudioHelper.boolMethod("isStreamMute", new Object[] { streamType }); if (null != ret) return ret; } return false; } /** @return The max volume for music, taking "safe volume," into consideration. */ protected int getMusicStreamMaxVolume() { return mAudioManager.getStreamMaxVolume(STREAM_MUSIC); /*if (mAudioHelper.isSafeMediaVolumeEnabled(getContext())) { final int index = getStreamVolume(STREAM_MUSIC); final int headsetMax = mAudioHelper.getSafeMediaVolumeIndex(); if (headsetMax > 0) { if (index > headsetMax) { return max; } else { return headsetMax; } } } return max;*/ } protected int getStreamMaxVolume(int streamType) { if (streamType == STREAM_MASTER) { return mVolumeManager.getSmallestMax(); } else if (streamType == STREAM_REMOTE_MUSIC) { Integer ret = mAudioHelper.intIServiceMethod("getRemoteStreamMaxVolume", null); if (null != ret) return ret; streamType = STREAM_MUSIC; } else if (streamType == USE_DEFAULT_STREAM_TYPE) { return Integer.MAX_VALUE; } // NOTE: this is done to deal with "safe volume." if (streamType == STREAM_MUSIC) return getMusicStreamMaxVolume(); return mAudioManager.getStreamMaxVolume(streamType); } protected int getStreamVolume(int streamType) { if (streamType == STREAM_MASTER) { return mVolumeManager.getManagedVolume(); } else if (streamType == STREAM_REMOTE_MUSIC) { Integer ret = mAudioHelper.intIServiceMethod("getRemoteStreamVolume", null); if (null != ret) return ret; streamType = STREAM_MUSIC; } else if (streamType == USE_DEFAULT_STREAM_TYPE) { return Integer.MIN_VALUE; // Can't report to the default. } return mAudioManager.getStreamVolume(streamType); } /** Proxy for {@link #setStreamVolume(int, int). */ protected void setVolume(int index) { setStreamVolume(AudioManager.USE_DEFAULT_STREAM_TYPE, index); } /** Proxy for {@link AudioManager#setStreamVolume(int,int,int).<br /> * Handles setting the relevant flags and calling the proper method. */ protected void setStreamVolume(int streamType, int index) { // NOTE: Master Volume needs to handle setting volumes differently. int flags = getFlags(streamType); if (oneVolume || streamType == STREAM_MASTER) { mVolumeManager.setVolumeSync(index, mVolumeManager.getSmallestMax()); return; } if (streamType == STREAM_REMOTE_MUSIC) { Object ret = mAudioHelper.serviceMethod("setRemoteStreamVolume", new Object[] { index }); if (null != ret) return; } if (streamType == Integer.MIN_VALUE) streamType = USE_DEFAULT_STREAM_TYPE; mAudioManager.setStreamVolume(streamType, index, flags); } protected int lastDirection; /** Proxy for {@link #adjustStreamVolume(int, int) */ protected void adjustVolume(int direction) { lastDirection = direction; // Same implementation of AudioManager#adjustVolume(int,int) LOGI("VolumePanel", "adjustVolume(" + direction + ")"); if (oneVolume) mVolumeManager.adjustVolumeSync(direction); else { /*if (isWiredHeadsetOn() && mAudioHelper.isSafeMediaVolumeEnabled(getContext())) { final int safeIndex = mAudioHelper.getSafeMediaVolumeIndex(); final int musicVolume = getStreamVolume(STREAM_MUSIC); final int max = getStreamMaxVolume(STREAM_MUSIC); if (musicVolume >= safeIndex) { LOGI("VolumePanel", "Bypassing safe volume index."); try { int ret = mAudioFlingerProxy.adjustStreamVolume(STREAM_MUSIC, direction, musicVolume, max); if (ret == AudioFlingerProxy.BAD_VALUE || ret == AudioFlingerProxy.CALIBRATION_ERROR) { adjustStreamVolume(direction, USE_DEFAULT_STREAM_TYPE); } else { int index = mAudioFlingerProxy.getStreamIndex(STREAM_MUSIC); LOGI("VolumePanel", "Stream index now: " + index); onVolumeChanged(STREAM_MUSIC, index, max); } } catch (RemoteException re) { LOGE("VolumePanel", "Error bypassing safe volume index.", re); adjustStreamVolume(direction, USE_DEFAULT_STREAM_TYPE); } } else { adjustStreamVolume(direction, USE_DEFAULT_STREAM_TYPE); } } else {*/ adjustStreamVolume(direction, USE_DEFAULT_STREAM_TYPE); //} } } /** Proxy for {@link AudioManager#adjustSuggestedStreamVolume(int,int,int). * Handles setting the relevant flags and calling the proper method. */ protected void adjustStreamVolume(int direction, int streamType) { if (hasDefaultStream()) streamType = defaultStream; if (mCallState == TelephonyManager.CALL_STATE_OFFHOOK) streamType = STREAM_VOICE_CALL; adjustSuggestedStreamVolume(direction, streamType, getFlags(streamType)); } /** Proxy for {@link AudioManager#adjustSuggestedStreamVolume(int,int,int). * Handles setting the relevant flags and calling the proper method. */ public void adjustSuggestedStreamVolume(int direction, int streamType, int flags) { if (null == mAudioManager) return; mAudioManager.adjustSuggestedStreamVolume(direction, streamType, flags); } protected int getFlags(int streamType) { // NOTE: NEVER use AudioManager.FLAG_SHOW_UI, this will display // the actual Android VolumePanel and ruin the ambiance. if (streamType == USE_DEFAULT_STREAM_TYPE) streamType = lastStreamType; int flags = 0; boolean streamAffectedByRinger; try { // Reported in Google Play developer console. streamAffectedByRinger = mAudioHelper.isStreamAffectedByRingerMode(streamType); } catch (Throwable t) { LOGE("VolumePanel", "Error determining ringer mode flag.", t); streamAffectedByRinger = false; } LOGI("VolumePanel", "getFlags(" + VolumeManager.getStreamName(streamType) + "), change=" + ringerMode + ", mRingerMode=" + mRingerMode + ", ringerAffected=" + streamAffectedByRinger); /*switch (ringerMode) { case Constants.RINGER_MODE_SILENT: if (streamAffectedByRinger) return AudioManager.FLAG_ALLOW_RINGER_MODES; return flags; }*/ // For the default, let's check the system settings. switch (mRingerMode) { case RINGER_MODE_NORMAL: flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE; break; case RINGER_MODE_VIBRATE: flags = AudioManager.FLAG_VIBRATE; break; } // Special mode, ALWAYS play a sound! if (ringerMode == Constants.RINGER_MODE_RING) { if ((flags & AudioManager.FLAG_PLAY_SOUND) != AudioManager.FLAG_PLAY_SOUND) { flags |= AudioManager.FLAG_PLAY_SOUND; } } else if (ringerMode == Constants.RINGER_MODE_SILENT) { if ((flags & AudioManager.FLAG_PLAY_SOUND) == AudioManager.FLAG_PLAY_SOUND) { flags &= ~AudioManager.FLAG_PLAY_SOUND; } } // Handle the addition of the ringer mode based on the stream type. /*if (streamAffectedByRinger) if (flags == 0) flags = AudioManager.FLAG_ALLOW_RINGER_MODES; else flags |= AudioManager.FLAG_ALLOW_RINGER_MODES;*/ return flags; } public int getPriorityMode() { return mPriorityMode; } protected int mPriorityMode = Constants.ZENMODE_ALL; protected void onPriorityModeChanged(int priorityMode) { LOGI(TAG, "onPriorityModeChanged(" + priorityMode + ')'); } /** @return The default {@link android.content.Intent#ACTION_MEDIA_BUTTON} receiver, or null. */ public ComponentName getMediaButtonReceiver() { String receiverName = Settings.System.getString( getContext().getContentResolver(), Constants.getMediaButtonReceiver()); if ((null != receiverName) && !receiverName.isEmpty()) return ComponentName.unflattenFromString(receiverName); return null; } // end localized methods /** @return True if the ringer is silent. */ public boolean isRingSilent() { return mRingIsSilent; } /** @return True if the device should vibrate while it is ringing. */ public boolean vibrateWhenRinging() { return mVibrateWhenRinging; } /** @return True if the updated value is different. */ private boolean updateVibrateWhenRinging() { boolean vibrateWhenRinging = (1 == Settings.System.getInt( getContext().getContentResolver(), Constants.KEY_VIBRATE, 0)); boolean ret = (mVibrateWhenRinging == vibrateWhenRinging); mVibrateWhenRinging = vibrateWhenRinging; return ret; } /** * Checks whether the phone is in silent mode, with or without vibrate. * @return true if phone is in silent mode, with or without vibrate. */ public boolean isSilentMode() { return (mRingerMode == RINGER_MODE_SILENT) || (mRingerMode == RINGER_MODE_VIBRATE); } protected boolean registeredOtto; private void hookIntoEvents() { Context context = getContext(); MainThreadBus.get().register(this); mAppTypeMonitor = new AppTypeMonitor(MediaStore.ACTION_IMAGE_CAPTURE, AlarmClock.ACTION_SET_ALARM); mAppTypeMonitor.register(context); mPriorityModeObserver = new GlobalSetting(context, mUiHandler, Constants.ZEN_MODE) { @Override protected void handleValueChanged(int value) { mPriorityMode = value; onPriorityModeChanged(value); } }; mPriorityMode = mPriorityModeObserver.getValue(); mPriorityModeObserver.setListening(true); registeredOtto = true; } // Initialize some basic audio states to be used onCreate and elsewhere. protected void initState() { mRingerMode = mAudioManager.getRingerMode(); mCallState = mTelephonyManager.getCallState(); mSpeakerphoneOn = mAudioManager.isSpeakerphoneOn(); mMusicActive = mAudioHelper.isLocalOrRemoteMusicActive(); RingerNotificationLink linkCheck = new RingerNotificationLink(); mNotificationRingerLink = linkCheck.apply(mAudioManager); SystemVolume systemVolume = new SystemVolume(); StreamResources.SystemStream.show(systemVolume.apply(mAudioManager)); updateVibrateWhenRinging(); loadSettings(); } /** * Load necessary settings. Avoid using {@link android.util.Property#set(Object, Object)} * and call {@code super}. Called in {@link #onCreate()} automatically. * */ public SettingsHelper loadSettings() { SettingsHelper settingsHelper = SettingsHelper.getInstance(getContext()); setMusicUri(settingsHelper.getProperty(VolumePanel.class, MUSIC_APP, musicUri)); // Music App? setAutoHideDuration(settingsHelper.getIntProperty(PopupWindow.class, TIMEOUT, getResources().getInteger(R.integer.volume_panel_timeout_default))); // 5 seconds default setRingerMode(settingsHelper.getIntProperty(VolumePanel.class, RINGER_MODE, Constants.RINGER_MODE_DEFAULT)); setOneVolume(settingsHelper.getProperty(VolumePanel.class, MASTER_VOLUME, false)); setLongPressActionDown(settingsHelper.getProperty(VolumePanel.class, ACTION_LONG_PRESS_VOLUME_DOWN, "")); setLongPressActionUp(settingsHelper.getProperty(VolumePanel.class, ACTION_LONG_PRESS_VOLUME_UP, "")); setColor(settingsHelper.getProperty(VolumePanel.class, COLOR, color)); setBackgroundColor(settingsHelper.getProperty(VolumePanel.class, BACKGROUND, backgroundColor)); setSeek(settingsHelper.getProperty(VolumePanel.class, SEEK, seek)); setNoLongPress(settingsHelper.getProperty(VolumePanel.class, NO_LONG_PRESS, noLongPress)); setHideFullscreen(settingsHelper.getProperty(VolumePanel.class, HIDE_FULLSCREEN, hideFullscreen)); setHideCamera(settingsHelper.getProperty(VolumePanel.class, HIDE_CAMERA, hideCamera)); setStretch(settingsHelper.getProperty(VolumePanel.class, STRETCH, stretch)); setDefaultStream(settingsHelper.getIntProperty(VolumePanel.class, DEFAULT_STREAM, defaultStream)); setLinkNotifRinger(settingsHelper.getProperty(VolumePanel.class, LINK_NOTIF_RINGER, linkNotifRinger)); setTertiaryColor(settingsHelper.getProperty(VolumePanel.class, TERTIARY, tertiaryColor)); setAlwaysExpanded(settingsHelper.getProperty(VolumePanel.class, ALWAYS_EXPANDED, alwaysExpanded)); setFirstReveal(settingsHelper.getProperty(VolumePanel.class, FIRST_REVEAL, firstReveal)); return settingsHelper; } /** @return True if the given setting has been set. */ protected boolean has(Property<VolumePanel, ?> prop) { SettingsHelper settingsHelper = SettingsHelper.getInstance(getContext()); return settingsHelper.hasProperty(VolumePanel.class, prop); } protected static final View.OnTouchListener noTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return true; } }; @Override public void onDestroy() { Context context = getContext(); try { if (null != mVolumeMediaReceiver) context.unregisterReceiver(mVolumeMediaReceiver); } catch (IllegalArgumentException iae) { LOGE("VolumePanel", "Could not unregister volume/ media receiver.", iae); } if (null != mAppTypeMonitor) mAppTypeMonitor.unregister(context); if (null != mPriorityModeObserver) mPriorityModeObserver.setListening(false); mPriorityModeObserver = null; mAudioManager = null; mAppTypeMonitor = null; mVolumeMediaReceiver = null; AudioHelper.freeResources(); if (null != mAudioHelper) mAudioHelper.setHandler(null); if (null != mMediaProviderDelegate) mMediaProviderDelegate.destroy(); mMediaProviderDelegate = null; mAudioHelper = null; try { if (registeredOtto) MainThreadBus.get().unregister(this); } catch (IllegalArgumentException e) { LOGE("VolumePanel", "Failed to unregister our VolumePanel from Otto."); } super.onDestroy(); } @Override protected WindowManager.LayoutParams getWindowParams() { if (null == mWindowAttributes) { mWindowAttributes = getWindowLayoutParams(); if (null != mWindowAttributes) { mWindowAttributes.type = ((isInteractive()) ? LayoutParams.TYPE_SYSTEM_ERROR : LayoutParams.TYPE_SYSTEM_OVERLAY); } } return mWindowAttributes; } /** @return True if this VolumePanel responds to long press events. */ public boolean respondsToLongPress() { return (!TextUtils.isEmpty(longPressActionDown) || !TextUtils.isEmpty(longPressActionUp)); } /** @return True if {@link me.barrasso.android.volume.MediaControllerService} is enabled. */ public boolean isNotificationListenerRunning() { return Utils.isMediaControllerRunning(getContext()); } public boolean isMusicActive() { return mMusicActive; } public String getLongPressActionUp() { return longPressActionUp; } public String getLongPressActionDown() { return longPressActionDown; } public void setLongPressActionUp(String actionUp) { longPressActionUp = actionUp; } public void setLongPressActionDown(String actionDown) { longPressActionDown = actionDown; } public boolean isStretch() { return stretch; } public void setStretch(boolean stretchIt) { stretch = stretchIt; } public void setDefaultStream(int stream) { defaultStream = stream; } public int getDefaultStream() { return defaultStream; } public boolean hasDefaultStream() { return (defaultStream >= 0); } public boolean isAlwaysExpanded() { return alwaysExpanded; } public void setAlwaysExpanded(boolean expand) { alwaysExpanded = expand; } protected int mKeyCodeDown = 0; protected boolean mIgnoreNextKeyUp = false; protected long mKeyTimeDown = 0; @Override public boolean onKey(View v, final int keyCode, KeyEvent event) { LOGI("VolumePanel", "onKey(" + keyCode + ")"); // Don't handle ANYTHING when a call is in progress! if (mCallState != TelephonyManager.CALL_STATE_IDLE) return super.onKey(v, keyCode, event); switch (keyCode) { // Handle the DOWN + MULTIPLE action (holding down). case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: final int adjust = ((keyCode == KeyEvent.KEYCODE_VOLUME_UP) ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER); switch (event.getAction()) { case KeyEvent.ACTION_DOWN: // If another key was pressed while holding on to // one volume key, we'll need to abort mission. if (mKeyCodeDown != 0) { mIgnoreNextKeyUp = true; mHandler.removeMessages(MSG_VOLUME_LONG_PRESS); return super.onKey(v, keyCode, event); } mKeyCodeDown = event.getKeyCode(); mKeyTimeDown = System.currentTimeMillis(); event.startTracking(); // NOTE: we'll allow long press events if we've set an action. boolean callIdle = (mCallState == TelephonyManager.CALL_STATE_IDLE); if (!noLongPress || hasLongPressAction(keyCode)) { mHandler.sendMessageDelayed(mHandler.obtainMessage( MSG_VOLUME_LONG_PRESS, event), ((callIdle && hasLongPressAction(keyCode)) ? mLongPressTimeout : mLongPressTimeout / 2)); } break; case KeyEvent.ACTION_UP: case KeyEvent.ACTION_MULTIPLE: boolean hasLongPress = mHandler.hasMessages(MSG_VOLUME_LONG_PRESS); mHandler.removeMessages(MSG_VOLUME_LONG_PRESS); boolean ignoreNextKeyUp = mIgnoreNextKeyUp; mIgnoreNextKeyUp = false; mKeyCodeDown = 0; // We've been told to let this one go. if (ignoreNextKeyUp || event.isCanceled()) { mKeyTimeDown = 0; return true; } if ((hasLongPress || noLongPress) && (System.currentTimeMillis() - mKeyTimeDown) < mLongPressTimeout) { mVolumeDirty = true; mKeyTimeDown = 0; if (!firstReveal || (firstReveal && isShowing())) { adjustVolume(adjust); show(); } else { reveal(); } } break; } break; case KeyEvent.KEYCODE_VOLUME_MUTE: switch (event.getAction()) { case KeyEvent.ACTION_UP: boolean mute = isMuted(STREAM_RING); mAudioManager.setStreamMute(STREAM_RING, !mute); mAudioManager.setStreamMute(STREAM_NOTIFICATION, !mute); mVolumeDirty = true; show(); break; } break; } return super.onKey(v, keyCode, event); } @Override public void closeSystemDialogs(String reason) { LOGI("VolumePanel", "closeSystemDialogs(" + reason + ')'); if (VolumePanel.class.getSimpleName().equals(reason) || getClass().getSimpleName().equals(reason) || getName().equals(reason)) return; super.closeSystemDialogs(reason); } /** A Volume {@link android.view.KeyEvent} has been long pressed. */ public void onVolumeLongPress(KeyEvent event) { if (null == event) return; // Set the action based on the KeyEvent. String longPressAction = null; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: longPressAction = longPressActionUp; break; case KeyEvent.KEYCODE_VOLUME_DOWN: longPressAction = longPressActionDown; break; } // If the event was volume up/ down, handle it. If the user is currently // in the middle of a call, only handle volume up/ down events. LOGI("VolumePanel", "onVolumeLongPress(" + event.getKeyCode() + ") action=" + longPressAction); boolean callIdle = (mCallState == TelephonyManager.CALL_STATE_IDLE); if (!TextUtils.isEmpty(longPressAction) && callIdle) { try { Intent action = Intent.parseUri(longPressAction, Intent.URI_INTENT_SCHEME); startActivity(action); } catch (URISyntaxException e) { LOGE("VolumePanel", "Error parsing long press action as an Intent.", e); } } else { // If we don't have a long press event, use this timeout as // a key timeout for volume manipulation. final int adjust = ((event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP) ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER); int flags = getFlags(lastStreamType); flags &= ~FLAG_PLAY_SOUND; flags &= ~FLAG_VIBRATE; // Adjust stream volume, but avoid making noises continuously. int stream = lastStreamType; if (hasDefaultStream()) stream = defaultStream; boolean ringerAffected = mAudioHelper.isStreamAffectedByRingerMode(stream); // Ringer mode transition for long-press actions /*if (ringerAffected) { int volume = getStreamVolume(stream); // If we're already at silent, stop listening. if ((adjust == ADJUST_LOWER) && (volume == 0) && (mRingerMode == AudioManager.RINGER_MODE_SILENT)) { return; } else if (adjust == ADJUST_RAISE && volume == 0) { int nextRinger = Utils.nextRingerMode(adjust, mRingerMode); mAudioManager.setRingerMode(nextRinger); } }*/ LOGI("VolumePanel", "[stream=" + stream + ", lastStream=" + lastStreamType + ", ringerAffected=" + ringerAffected + ']'); adjustSuggestedStreamVolume(adjust, stream, flags); mVolumeDirty = true; // This is needed to show our volume change! mIgnoreNextKeyUp = false; // This is key to avoid infinite loops! if (!noLongPress || hasLongPressAction(event.getKeyCode())) { mHandler.sendMessageDelayed(mHandler.obtainMessage( MSG_VOLUME_LONG_PRESS, event), (mLongPressTimeout / 3)); } show(); } } public boolean hasLongPressAction() { return !(TextUtils.isEmpty(longPressActionDown) || TextUtils.isEmpty(longPressActionUp)); } public boolean hasLongPressAction(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: return !TextUtils.isEmpty(longPressActionDown); case KeyEvent.KEYCODE_VOLUME_UP: return !TextUtils.isEmpty(longPressActionUp); } return false; } /** {@link Handler} for audio/ media event handling. */ /*package*/ final class VolumeMediaHandler extends Handler { public VolumeMediaHandler(Looper loopah) { super(loopah); } // All calls will be handles on the main looper (UI Thread). @SuppressWarnings("unchecked") @Override public void handleMessage(Message msg) { LOGI("VolumePanel", "handleMessage(" + VolumeMediaReceiver.getMsgName(msg.what) + ')'); switch (msg.what) { // obj: int[3], type, vol, prevVol case MSG_VOLUME_CHANGED: int[] vals = (int[]) msg.obj; // If we just got another event in an unreasonably short period of time, // close the system panel and don't handle this event. final VolumeChangeInfo lastChangeInfo = mLastVolumeChange; mLastVolumeChange = new VolumeChangeInfo(vals[0], vals[1], vals[2]); if (null != lastChangeInfo && vals[1] == vals[2]) { if ((System.currentTimeMillis() - lastChangeInfo.mEventTime) < 100) { mAudioHelper.closeSystemDialogs(getContext(), VolumePanel.this.getClass().getSimpleName()); removeMessages(MSG_VOLUME_CHANGED); return; } } if (mVolumeDirty) { onVolumeChanged(vals[0], vals[1], vals[2]); mVolumeDirty = false; } break; // arg1: ringer mode case MSG_RINGER_MODE_CHANGED: mLastRingerMode = mRingerMode; mRingerMode = msg.arg1; onRingerModeChange(mRingerMode); break; // arg1: stream type // arg2: mute mode (boolean 1/0) case MSG_MUTE_CHANGED: onMuteChanged(msg.arg1, (msg.arg2 == 1)); break; // arg1: speakerphone (0 == off, else on) case MSG_SPEAKERPHONE_CHANGED: onSpeakerphoneChange(!(msg.arg1 == 0)); break; // arg1: vibrate type // arg2: vibrate settings case MSG_VIBRATE_MODE_CHANGED: onVibrateModeChange(msg.arg1, msg.arg2); break; // obj: KeyEvent? case MSG_MEDIA_BUTTON_EVENT: break; // obj: KeyEvent case MSG_VOLUME_LONG_PRESS: KeyEvent event = (KeyEvent) msg.obj; if (noLongPress && !hasLongPressAction(event.getKeyCode())) return; mIgnoreNextKeyUp = true; onVolumeLongPress(event); break; // obj: Pair<Metadata, PlaybackInfo> case MSG_PLAY_STATE_CHANGED: // Only accept events if the delegate isn't active. if (null == mMediaProviderDelegate || !mMediaProviderDelegate.isClientActive()) { Pair<MediaMetadataCompat, PlaybackStateCompat> pair = (Pair<MediaMetadataCompat, PlaybackStateCompat>) msg.obj; onPlayStateChanged(pair); } break; // arg1: keycode case MSG_DISPATCH_KEYEVENT: if (Utils.isMediaKeyCode(msg.arg1)) dispatchMediaKeyEvent(msg.arg1); break; // arg1: call state case MSG_CALL_STATE_CHANGED: mCallState = msg.arg1; onCallStateChange(mCallState); break; // obj: audio routes info case MSG_AUDIO_ROUTES_CHANGED: AudioRoutesInfo info = (AudioRoutesInfo) msg.obj; onAudioRoutesChanged(info); break; case MSG_HEADSET_PLUG: int state = msg.arg1; onHeadsetPlug(state); break; case MSG_USER_PRESENT: locked = false; onLockChange(); break; case MSG_ALARM_CHANGED: onAlarmChanged(); break; } } } public static void attachPlaybackListeners(ViewGroup layout, View.OnClickListener listener) { attachPlaybackListeners(layout, listener, null); } public static void attachPlaybackListeners(ViewGroup layout, View.OnClickListener listener, View.OnLongClickListener longListener) { // To avoid strange issues with View.OnClick not being invoked, we'll proxy the call // to OnTouchListener and test for a click based on distance & time. Set<View> views = new HashSet<View>(); views.add(layout.findViewById(R.id.media_previous)); views.add(layout.findViewById(R.id.media_play_pause)); views.add(layout.findViewById(R.id.media_next)); OnTouchClickListener mListener = new OnTouchClickListener(listener, longListener); for (View view : views) if (null != view) view.setOnTouchListener(mListener); } protected static void setVisibilityBasedOnFlag(View view, long flags, long flag) { if ((flags & flag) != 0) { view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } /** * {@link View.OnClickListener} for media buttons. * @see {@link R.id#media_previous} * @see {@link R.id#media_play_pause} * @see {@link R.id#media_next} */ /*package*/ final class MediaButtonClickListener implements View.OnClickListener { public MediaButtonClickListener() { } @Override public void onClick(View v) { LOGD("VolumePanel", "onClick(" + v.getId() + ')'); Integer keyCode = null; switch (v.getId()) { case R.id.media_previous: keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; break; case R.id.media_play_pause: keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; break; case R.id.media_next: keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; break; default: return; } onUserInteraction(); Message.obtain(mHandler, MSG_DISPATCH_KEYEVENT, keyCode, 0).sendToTarget(); } } /** Sends a media KeyEvent to the system. */ public void dispatchMediaKeyEvent(final int keyCode) { LOGI("VolumePanel", "dispatchMediaKeyEvent(" + keyCode + ')'); // NOTE: Avoid using the RemoteController methods for dispatching KeyEvents // this opens the media app and ruins the ambiance. mAudioHelper.dispatchMediaKeyEvent(getContext(), keyCode); onUserInteraction(); onDispatchMediaKeyEvent(keyCode); } /** Same as {@link #show()}, but meant when volume key's aren't pressed. */ public void reveal() { if (!isEnabled()) return; if (isShowing()) { onUserInteraction(); return; } mVolumeDirty = true; if (hasDefaultStream()) { mAudioManager.adjustStreamVolume(defaultStream, ADJUST_SAME, 0); } else { mAudioManager.adjustVolume(ADJUST_SAME, 0); } show(); } @Override public void show() { if (!isEnabled() || (mCallState != TelephonyManager.CALL_STATE_IDLE)) return; // If we've been told to hide, we'll do it. if (null != mLastVolumeChange && mLastVolumeChange.mStreamType == STREAM_MUSIC && hideFullscreen && fullscreen) { LOGI("VolumePanel", "Not showing panel, hiding for fullscreen media."); return; } // Only show the panel if the screen is on. if (null != pWindowManager && pWindowManager.isScreenOn()) { if (null != mMediaProviderDelegate) mMediaProviderDelegate.acquire(getWindowWidth(), getWindowHeight()); super.show(); // NOTE: snapshots can be taken here, each time the panel is shown. // snapshot(); } } @Override public void hide() { if (null != mMediaProviderDelegate) mMediaProviderDelegate.relinquish(false); if (null != mAudioHelper) mAudioHelper.forceVolumeControlStream(-1); super.hide(); } protected boolean locked; // The lock screen/ keyguard state has changed. protected void onLockChange() { } public boolean isLocked() { return locked; } @Override public void screen(boolean on) { super.screen(on); if (!on) { locked = true; onLockChange(); } if (null != mVolumeMediaReceiver) mVolumeMediaReceiver.setScreen(on); // NOTE: always give up keyguard lock when the screen turns off! if (!on && null != mKeyguardLock) { mKeyguardLock.reenableKeyguard(); mKeyguardLock = null; } if (null != mHandler) mHandler.removeMessages(MSG_VOLUME_LONG_PRESS); mCallState = mTelephonyManager.getCallState(); checkCallState(); } /** @return This class' {@link ComponentName} for identification. */ public ComponentName getComponentName() { return new ComponentName(pWindowManager.getContext().getPackageName(), getClass().getName()); } /** Sets the color of this VolumePanel */ public void setColor(final int mColor) { color = mColor; } /** @return The color of this VolumePanel. */ public int getColor() { return color; } /** Sets the background color of this VolumePanel */ public void setBackgroundColor(final int mColor) { backgroundColor = mColor; } /** @return The background color of this VolumePanel. */ public int getBackgroundColor() { return backgroundColor; } /** Sets the tertiary color of this VolumePanel */ public void setTertiaryColor(final int mColor) { tertiaryColor = mColor; } /** @return The tertiary color of this VolumePanel. */ public int getTertiaryColor() { return tertiaryColor; } protected String musicUri; public String getMusicUri() { return musicUri; } public void setMusicUri(String uri) { musicUri = uri; } /*package*/ void abandonMusicUri() { MUSIC_APP.set(this, null); } public void setSeek(final boolean shouldSeek) { seek = shouldSeek; } public boolean isSeek() { return seek; } public boolean isOneVolume() { return oneVolume; } public void setOneVolume(final boolean master) { oneVolume = master; // When we create this panel, sync all volumes from the get-go.sss if (oneVolume) { mVolumeManager.adjustVolumeSync(AudioManager.ADJUST_SAME); } } public boolean isNoLongPress() { return noLongPress; } public void setNoLongPress(boolean mNoLongPress) { noLongPress = mNoLongPress; mIgnoreNextKeyUp = false; if (null != mHandler) mHandler.removeMessages(MSG_VOLUME_LONG_PRESS); } public void setHideCamera(boolean camera) { hideCamera = camera; } public boolean getHideCamera() { return hideCamera; } public void setHideFullscreen(boolean hideFS) { hideFullscreen = hideFS; } public boolean getHideFullscreen() { return hideFullscreen; } public void setFirstReveal(boolean reveal) { firstReveal = reveal; } public boolean getFirstReveal() { return firstReveal; } public int getRingerMode() { return ringerMode; } public void setRingerMode(final int mode) { ringerMode = mode; } public void setLinkNotifRinger(boolean link) { linkNotifRinger = link; } public boolean isLinkNotifRinger() { return linkNotifRinger; } protected String musicPackageName; /*package*/ boolean launchMusicApp() { // First, see if we've got a package name for the music app. // This is set generally from RemoteController. if (!TextUtils.isEmpty(musicPackageName)) { PackageManager mPM = getContext().getPackageManager(); Intent launch = mPM.getLaunchIntentForPackage(musicPackageName); if (null != launch) { boolean success = startActivity(launch); if (success) return true; musicPackageName = null; } } // If the user has a preference, try it. Otherwise, fall to default. if (!TextUtils.isEmpty(musicUri)) { try { Intent music = Intent.parseUri(musicUri, Intent.URI_INTENT_SCHEME); boolean success = startActivity(music); if (success) return true; abandonMusicUri(); } catch (URISyntaxException e) { LOGE("VolumePanel", "Error parsing music app URI.", e); } } Intent music = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MUSIC); return startActivity(music); } private KeyguardManager.KeyguardLock mKeyguardLock; /** Start an activity. Returns true if successful. */ @SuppressWarnings("deprecation") protected boolean startActivity(Intent intent) { Context context = getContext(); if (null == context || null == intent) return false; // Disable the Keyguard if necessary. KeyguardManager mKM = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); if (mKM.isKeyguardLocked() && !mKM.isKeyguardSecure()) { mKeyguardLock = mKM.newKeyguardLock(getName()); mKeyguardLock.disableKeyguard(); } try { // Necessary because we're in a background Service! intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); if (intent.resolveActivity(context.getPackageManager()) != null) { context.startActivity(intent); return true; } } catch (ActivityNotFoundException anfe) { LOGE("VolumePanel", "Error launching Intent: " + intent.toString(), anfe); } catch (SecurityException se) { LOGE("VolumePanel", "Error launching Intent: " + intent.toString(), se); Toast.makeText(context, R.string.permission_error, Toast.LENGTH_SHORT).show(); } return false; } // Localized from android.view.VolumePanel protected void onVolumeChanged(int streamType, int index, int prevIndex) { onVolumeChanged(streamType, index, prevIndex, false); } // Notified when the volume for a given stream changes. @SuppressWarnings("deprecation") protected void onVolumeChanged(int streamType, int index, int prevIndex, boolean ringerModeChange) { if (!mAudioHelper.isVoiceCapable() && (streamType == STREAM_RING)) { streamType = STREAM_NOTIFICATION; } mRingIsSilent = false; lastStreamType = streamType; // get max volume for progress bar int max = getStreamMaxVolume(streamType); LOGI("VolumePanel", "onVolumeChanged(" + VolumeManager.getStreamName(streamType) + "), index: " + index + ", max: " + max + ", prev: " + prevIndex + ", ringerModeChange: " + ringerModeChange); boolean headphonesActive = false; // TRACK: the volume change event. switch (streamType) { case AudioManager.STREAM_NOTIFICATION: case AudioManager.STREAM_RING: try { Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( getContext(), ((streamType == AudioManager.STREAM_NOTIFICATION) ? RingtoneManager.TYPE_NOTIFICATION : RingtoneManager.TYPE_RINGTONE) ); if (ringuri == null) { mRingIsSilent = true; } } catch (SecurityException se) { LOGE("VolumePanel", "Error checking the current ringtone.", se); } // Device has separate ringer/ notification, but wants them linked. // Make sure the volume actually changed, otherwise we'll end up in a loop. if (!ringerModeChange && prevIndex != index) { if (linkNotifRinger && !mNotificationRingerLink) { mVolumeDirty = false; int newStream = ((streamType == AudioManager.STREAM_RING) ? AudioManager.STREAM_NOTIFICATION : AudioManager.STREAM_RING); LOGI("VolumePanel", "Linking notif-ringer volume: " + VolumeManager.getStreamName(newStream) + ", index: " + index); setStreamVolume(newStream, index); } } // If we've hit volume down again, let's go silent. if (!ringerModeChange && prevIndex == index && index == 0 && lastDirection == ADJUST_LOWER) { if (!Utils.HasChronicallyStupidWayToSetPriorityModeWhenItShouldBeSilentMode()) { LOGI("VolumePanel", "Volume at 0, setting ringer mode (" + mRingerMode + ')'); if (mAudioHelper.hasVibrator()) { if (mRingerMode == RINGER_MODE_VIBRATE) mAudioManager.setRingerMode(RINGER_MODE_SILENT); else if (mRingerMode == RINGER_MODE_NORMAL) mAudioManager.setRingerMode(RINGER_MODE_VIBRATE); } else { if (mRingerMode == RINGER_MODE_NORMAL) mAudioManager.setRingerMode(RINGER_MODE_SILENT); } } } break; case AudioManager.STREAM_MUSIC: { // Special case for when Bluetooth is active for music int devBit = (Constants.DEVICE_OUT_BLUETOOTH_A2DP | Constants.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | Constants.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER); int headBit = (Constants.DEVICE_OUT_WIRED_HEADSET | Constants.DEVICE_OUT_WIRED_HEADPHONE); Integer ret = mAudioHelper.intMethod("getDevicesForStream", new Object[] { STREAM_MUSIC }); if (mAudioManager.isBluetoothA2dpOn() || (null != ret && (ret & devBit) != 0)) { setMusicIcon(MusicMode.BLUETOOTH); } else if (mAudioManager.isWiredHeadsetOn() || (null != ret && (ret & headBit) != 0)) { setMusicIcon(MusicMode.HEADSET); headphonesActive = true; } else { setMusicIcon(MusicMode.DEFAULT); } break; } case AudioManager.STREAM_ALARM: break; } // Avoid issues with constant expression in switch-case statements. if (streamType == STREAM_BLUETOOTH_SCO || streamType == AudioManager.STREAM_VOICE_CALL) { // For in-call voice call volume, there is no inaudible volume. // Rescale the UI control so the progress bar doesn't go all // the way to zero and don't show the mute icon. index = Math.min(++index, max); } // In "safe volume" mode, display a different icon. if (index >= mAudioHelper.getSafeMediaVolumeIndex() && headphonesActive && mAudioHelper.isSafeMediaVolumeEnabled(getContext())) { setMusicIcon(MusicMode.SAFE_VOLUME); } // Map out stream volume for calibration. // mAudioFlingerProxy.mapStreamIndex(streamType, index); // Inform the UI of the change, but if we're managing the volume // as a "Master Volume," we'll need to handle it specially. mVolumeManager.setVolume(streamType, index); if (!isShowing() && null != mAudioHelper) { int stream = (streamType == STREAM_REMOTE_MUSIC) ? -1 : streamType; // when the stream is for remote playback, use -1 to reset the stream type evaluation mAudioHelper.forceVolumeControlStream(stream); } if (oneVolume) { // TODO: Sync all volumes when the screen is off. At present, the only // stream that informs us of a change when the screen is off is the music stream. if (!pWindowManager.isScreenOn() && streamType == STREAM_MUSIC) { mVolumeManager.syncToStream(STREAM_MUSIC); } LOGD("VolumePanel", mVolumeManager.toString()); onStreamVolumeChange(STREAM_MASTER, mVolumeManager.getManagedVolume(), mVolumeManager.getManagedMaxVolume()); } else { LOGD("VolumePanel", mVolumeManager.toString()); onStreamVolumeChange(streamType, index, max); } } protected static enum MusicMode { DEFAULT(R.drawable.ic_audio_vol, R.drawable.ic_audio_vol_mute, R.string.volume_icon_description_media), BLUETOOTH(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute, R.string.volume_icon_description_bluetooth), HEADSET(R.drawable.ic_action_headphones, R.drawable.ic_action_headphones_mute, R.string.volume_icon_description_media), SAFE_VOLUME(R.drawable.ic_dialog_alert, R.drawable.ic_dialog_alert, R.string.safe_volume); public int iconResId; public int iconMuteResId; public int descResId; private MusicMode(int iconResId, int iconMuteResId, int descResId) { this.iconResId = iconResId; this.iconMuteResId = iconMuteResId; this.descResId = descResId; } } /* * Switch between icons because Bluetooth music is same as music volume, but with different icons. */ protected void setMusicIcon(MusicMode mode) { LOGI("VolumePanel", "setMusicIcon(" + mode.name() + ')'); if (mode.iconResId > 0) StreamResources.MediaStream.setIconRes(mode.iconResId); if (mode.iconMuteResId > 0) StreamResources.MediaStream.setIconMuteRes(mode.iconMuteResId); if (mode.descResId > 0) StreamResources.MediaStream.setDescRes(mode.descResId); } /** @see android.media.IAudioRoutesObserver */ public void onAudioRoutesChanged(AudioRoutesInfo info) { LOGI("VolumePanel", "onAudioRoutesChanged(" + info.toString() + ')'); } // ========== Media Router ========== protected CharSequence getStreamName(StreamResources resources) { return getContext().getString(resources.getDescRes()); } // Simple PhoneStateListener to handle when a call begins and ends. private static class CallStateListener extends PhoneStateListener { private final Handler mHandler; public CallStateListener(Handler handler) { mHandler = handler; } @Override public void onCallStateChanged(int state, String incomingNumber) { Message.obtain(mHandler, MSG_CALL_STATE_CHANGED, state).sendToTarget(); } } // Used to detect when a remote audio stream is attached. @SuppressWarnings("unused") private static class MediaRouteListener extends MediaRouter.SimpleCallback { protected int mCurrentRouteType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; protected MediaRouter.RouteInfo mCurrentRouteInfo; public MediaRouteListener() { super(); } public MediaRouter.RouteInfo getCurrentRoute() { return mCurrentRouteInfo; } /*package*/ void refreshRoute() { if (null == mCurrentRouteInfo) return; LOGI("MediaRouteListener", "refreshRoute(name=" + mCurrentRouteInfo.getName() + ", type=" + mCurrentRouteInfo.getPlaybackType() + ", stream=" + mCurrentRouteInfo.getPlaybackStream() + ")"); } @Override public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { LOGD("MediaRouteListener", "onRouteSelected(" + type + ", " + info.getName() + ")"); mCurrentRouteInfo = info; mCurrentRouteType = type; refreshRoute(); } @Override public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) { LOGD("MediaRouteListener", "onRouteUnselected(" + type + ", " + info.getName() + ")"); mCurrentRouteInfo = null; mCurrentRouteType = type; refreshRoute(); } @Override public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) { LOGD("MediaRouteListener", "onRouteVolumeChanged(" + info.getName() + ")"); mCurrentRouteInfo = info; refreshRoute(); } } // ========== SeekBar ========== /** * Notification that the progress level has changed. Clients can use the fromUser parameter * to distinguish user-initiated changes from those that occurred programmatically. */ @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { LOGD("VolumePanel", "onProgressChanged(" + progress + ")"); final Object tag = seekBar.getTag(); if (fromUser && tag instanceof StreamResources) { StreamResources sr = (StreamResources) tag; if (getStreamVolume(sr.getStreamType()) != progress) { setStreamVolume(sr.getStreamType(), progress); mVolumeDirty = true; } } onUserInteraction(); } /** * Notification that the user has started a touch gesture. Clients may want to use this * to disable advancing the seekbar. */ @Override public void onStartTrackingTouch(SeekBar seekBar) { onUserInteraction(); } /** * Notification that the user has finished a touch gesture. Clients may want to use this * to re-enable advancing the seekbar. */ @Override public void onStopTrackingTouch(SeekBar seekBar) { final Object tag = seekBar.getTag(); if (tag instanceof StreamResources) { StreamResources sr = (StreamResources) 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 (sr.getStreamType() == STREAM_REMOTE_MUSIC) { seekBar.setProgress(getStreamVolume(STREAM_REMOTE_MUSIC)); } } onUserInteraction(); } /** Saves a snapshot of this VolumePanel. */ @SuppressWarnings("unused") protected void snapshot() { // NOTE: this is performed synchronously. Aside from having no use in a production // app, it's just dang slow. NEVER call this unless it's for taking screenshots. Bitmap screen = Utils.loadBitmapFromViewCache(peekDecor()); File dir = getContext().getCacheDir(); File artFile = new File(dir, getName() + ".png"); OutputStream fos = null; try { fos = new BufferedOutputStream(new FileOutputStream(artFile)); screen.compress(Bitmap.CompressFormat.PNG, 100, fos); screen.recycle(); } catch (FileNotFoundException fnf) { LOGE("VolumePanel", "Cannot find " + artFile.getAbsolutePath(), fnf); } finally { try { if (null != fos) fos.close(); } catch (IOException ioe) { LOGE("VolumePanel", "Error closing snapshot OutputStream.", ioe); } } } // ========== ABSTRACT METHODS ========== /* * @param ringerMode The current ringtone mode, one of {@link AudioManager#RINGER_MODE_NORMAL}, * {@link AudioManager#RINGER_MODE_SILENT}, or {@link AudioManager#RINGER_MODE_VIBRATE}. */ public void onRingerModeChange(int ringerMode) { switch (ringerMode) { case RINGER_MODE_VIBRATE: StreamResources.RingerStream.setIconMuteRes(R.drawable.ic_audio_ring_notif_vibrate); StreamResources.NotificationStream.setIconMuteRes(R.drawable.ic_audio_ring_notif_vibrate); // NOTE: do a little vibrate when the ringer mode has been reached. if (isEnabled()) mAudioHelper.vibrate(VIBRATE_DURATION); // From VolumePanel break; case RINGER_MODE_SILENT: default: StreamResources.RingerStream.setIconMuteRes(R.drawable.ic_audio_phone_mute); StreamResources.NotificationStream.setIconMuteRes(R.drawable.ic_audio_notification_mute_am); break; } // Make sure we update immediately. if (null != mLastVolumeChange && isEnabled()) { onVolumeChanged(mLastVolumeChange.mStreamType, mLastVolumeChange.mToVolume, mLastVolumeChange.mFromVolume, true); } } /** The speakerphone mode has changed (on or off). */ public void onSpeakerphoneChange(boolean on) { } // NOTE: This is often handled as the MASTER mute has been set. /** The mute for a given volume stream has changed. */ public void onMuteChanged(int streamType, boolean mute) { } /** * The vibrate mode has changed for a given stream/ type. * @see {@link AudioManager#EXTRA_VIBRATE_TYPE} * @see {@link AudioManager#EXTRA_VIBRATE_SETTING} */ public void onVibrateModeChange(int vibrateType, int vibrateSetting) { } /** @see {@link android.telephony.TelephonyManager#getCallState()} */ public int getCallState() { return mCallState; } // When the user gets a call while the screen was off, disable. protected void checkCallState() { LOGI("VolumePanel", "checkCallState()"); if (mCallState != TelephonyManager.CALL_STATE_IDLE) { setEnabled(false); } } /** * Proxy for {@link android.telephony.PhoneStateListener}, to be notified when * the device's call state has changed. * @see #mCallState */ public void onCallStateChange(int callState) { LOGI("VolumePanel", "onCallStateChange(" + callState + ')'); mCallState = callState; mHandler.removeMessages(MSG_VOLUME_LONG_PRESS); // TRACK: when the user starts & ends a phone call. // When the user ends a call, let's take over volume again. if (callState == TelephonyManager.CALL_STATE_IDLE) { setEnabled(true); } else { setEnabled(false); hide(); } } /** * The volume for a given stream has changed. Will not be called * if a client directly requests the change in volume. * * @param streamType The stream whose volume index should be set. * @param volume The volume index to set. * @param max The largest valid value for this stream's audio. */ abstract void onStreamVolumeChange(int streamType, int volume, int max); /** * @return True if the {@link VolumePanel} is interactive with the * user (i.e. wants to accept touch events). */ abstract boolean isInteractive(); /** @return True if this VolumePanel supports media playback. */ public boolean supportsMediaPlayback() { return false; } protected Pair<MediaMetadataCompat, PlaybackStateCompat> mMediaInfo; /** * Notified when the play state has changed. Unfortunately, without a standardized API this * may or may not be called (with or without all fields), and may only work for certain * apps/ music players. */ public void onPlayStateChanged(Pair<MediaMetadataCompat, PlaybackStateCompat> mediaInfo) { LOGI("VolumePanel", "onPlayStateChanged()"); // If the event was just an update to the play state, handle accordingly. mMusicActive = RemoteControlCompat.isPlaying(mediaInfo.second); LOGI("VolumePanel", "isPlayState(playing=" + mMusicActive + ")"); // Update the play state if we've been given one. if (null != mAudioHelper && null == mediaInfo.second) mMusicActive = mAudioHelper.isLocalOrRemoteMusicActive(); // Update the music package name based on RemoteController/ magic. if (null != mediaInfo.first) musicPackageName = mediaInfo.first.getString(RemoteControlCompat.METADATA_KEY_PACKAGE); // TRACK: when the user starts and ends playing a song. mMediaInfo = mediaInfo; } protected void onHeadsetPlug(int state) { LOGI("VolumePanel", "onHeadsetPlug(" + state + ')'); } protected String mCurrentPackageName; protected String mCurrentActivityClass; @Override public void setEnabled(boolean enable) { LOGI("VolumePanel", "setEnabled(" + enable + ")"); super.setEnabled(enable); } public void enable() { setEnabled(!mAppTypeMonitor.doesPackageRespondToAny(mCurrentPackageName)); checkCallState(); } public void onTopAppChanged(VolumeAccessibilityService.TopApp app) { LOGI("VolumePanel", "onTopChanged(" + app.mCurrentPackage + '/' + app.mCurrentActivityClass + ')'); mCurrentActivityClass = app.mCurrentActivityClass; mCurrentPackageName = app.mCurrentPackage; { // CASE: Snapchat is special because it's popular, but they don't use Intents. if ("com.snapchat.android".equals(mCurrentPackageName) && "com.snapchat.android.LandingPageActivity".equals(mCurrentActivityClass)) { setEnabled(false); return; } // If the top app is a camera or alarm app, disable this volume panel. setEnabled(!mAppTypeMonitor.doesPackageRespondToAny(mCurrentPackageName)); } } protected long mNextAlarmTimeMillis; protected void onAlarmChanged() { String nextAlarm = Settings.System.getString(getContext().getContentResolver(), Settings.System.NEXT_ALARM_FORMATTED); LOGI("VolumePanel", "onAlarmChanged(" + nextAlarm + ')'); DateFormat format = new SimpleDateFormat("EEE hh:mm aa", Locale.US); try { Date date = format.parse(nextAlarm); mNextAlarmTimeMillis = date.getTime(); } catch (ParseException e) { mNextAlarmTimeMillis = 0; LOGE("VolumePanel", "Error parsing next alarm from Settings.System", e); } } /** * @param minutes The number of minutes to check the time difference. * @return True if the alarm is within that range of time. */ /*protected boolean isWithinMinutesOfNextAlarm(final int minutes) { if (mNextAlarmTimeMillis == 0) return false; long curTime = System.currentTimeMillis(); long diff = Math.abs(mNextAlarmTimeMillis - curTime); long minute = (1000 * 60); // 60s * 1000ms return (diff < (minutes * minute)); }*/ public void onFullscreenChange(boolean fullscreen) { LOGI("VolumePanel", "onFullscreenChange(" + fullscreen + ')'); this.fullscreen = fullscreen; if (isShowing() && fullscreen && hideFullscreen) hide(); } /** @return The {@link WindowManager.LayoutParams} for displaying this volume panel. */ abstract WindowManager.LayoutParams getWindowLayoutParams(); /** Notified when {@link android.media.AudioManager#dispatchMediaKeyEvent(android.view.KeyEvent)} is invoked. */ public void onDispatchMediaKeyEvent(int keyCode) { } }