Android6.0 源码修改之Settings音量调节界面增加通话音量调节
前言
今天客户提了个需求,因为我们的设备在正常情况下无法调节通话音量,只有在打电话过程中,按物理音量加减键才能出现调节通话音量seekBar,很不方便,于是乎需求就来了。需要优化两个地方
1、在正常情况下,按物理音量加减键都显示 通话音量调节seekBar,可方便快速调节通话音量
2、在Settings中提示音界面点击设置进入,增加通话音量调节seekBar
修改前
修改后
实现
第一个功能
先来完成第一个功能,还是通过Hierarchy View查看布局结构,查找到布局文件id为volume_dialog,通过在源码中搜索找到位于SystemUI中,volume_dialog.xml
源码位置 frameworks\base\packages\SystemUI\res\layout\volume_dialog.xml
对应的java类为 frameworks\base\packages\SystemUI\src\com\android\systemui\volume\VolumeDialog.java
修改代码
addRow(AudioManager.STREAM_VOICE_CALL, R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, true);
原来的第四个参数为false,修改为true即可显示通话音量seekBar
为了便于说明,我们跟进addRow()中查看
private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) { final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important); if (!mRows.isEmpty()) { final View v = new View(mContext); v.setId(android.R.id.background); final int h = mContext.getResources() .getDimensionPixelSize(R.dimen.volume_slider_interspacing); final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, h); mDialogContentView.addView(v, mDialogContentView.getChildCount() - 1, lp); row.space = v; } ... }
传递的参数对应important,从字面意思理解重要对应显示,继续查看initRow都做了什么
private VolumeRow initRow(final int stream, int iconRes, int iconMuteRes, boolean important) { final VolumeRow row = new VolumeRow(); row.stream = stream; row.iconRes = iconRes; row.iconMuteRes = iconMuteRes; row.important = important; row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null); row.view.setTag(row); row.header = (TextView) row.view.findViewById(R.id.volume_row_header); mSpTexts.add(row.header); row.slider = (SeekBar) row.view.findViewById(R.id.volume_row_slider); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); // forward events above the slider into the slider row.view.setOnTouchListener(new OnTouchListener() { private final Rect mSliderHitRect = new Rect(); private boolean mDragging; @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { row.slider.getHitRect(mSliderHitRect); if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_DOWN && event.getY() < mSliderHitRect.top) { mDragging = true; } if (mDragging) { event.offsetLocation(-mSliderHitRect.left, -mSliderHitRect.top); row.slider.dispatchTouchEvent(event); if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { mDragging = false; } return true; } return false; } }); row.icon = (ImageButton) row.view.findViewById(R.id.volume_row_icon); row.icon.setImageResource(iconRes); row.icon.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState); mController.setActiveStream(row.stream); if (row.stream == AudioManager.STREAM_RING) { final boolean hasVibrator = mController.hasVibrator(); if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { if (hasVibrator) { mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); } else { final boolean wasZero = row.ss.level == 0; mController.setStreamVolume(stream, wasZero ? row.lastAudibleLevel : 0); } } else { mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); if (row.ss.level == 0) { mController.setStreamVolume(stream, 1); } } } else { final boolean vmute = row.ss.level == 0; mController.setStreamVolume(stream, vmute ? row.lastAudibleLevel : 0); } row.userAttempt = 0; // reset the grace period, slider should update immediately } }); row.settingsButton = (ImageButton) row.view.findViewById(R.id.volume_settings_button); row.settingsButton.setOnClickListener(mClickSettings); return row; }
从上面可看出,将一些变量都保存到了VolumeRow中,设置了icon的点击事件,将当前对应的音量类型设置为最低(禁音), 设置seekBar的改变事件。通过过滤日志,查找到控制音量类型的显示和隐藏的代码块updateRowsH()
private boolean isVisibleH(VolumeRow row, boolean isActive) { return mExpanded && row.view.getVisibility() == View.VISIBLE || (mExpanded && (row.important || isActive)) || !mExpanded && isActive; } private void updateRowsH() { if (D.BUG) Log.d(TAG, "updateRowsH"); final VolumeRow activeRow = getActiveRow(); updateFooterH(); updateExpandButtonH(); if (!mShowing) { trimObsoleteH(); } // apply changes to all rows for (VolumeRow row : mRows) { final boolean isActive = row == activeRow; final boolean visible = isVisibleH(row, isActive); Log.e(TAG, "row==" + row.stream + " isActive=="+isActive + " visible="+visible); Util.setVisOrGone(row.view, visible); Util.setVisOrGone(row.space, visible && mExpanded); final int expandButtonRes = mExpanded ? R.drawable.ic_volume_settings : 0; if (expandButtonRes != row.cachedExpandButtonRes) { row.cachedExpandButtonRes = expandButtonRes; if (expandButtonRes == 0) { row.settingsButton.setImageDrawable(null); } else { row.settingsButton.setImageResource(expandButtonRes); } } Util.setVisOrInvis(row.settingsButton, false); updateVolumeRowHeaderVisibleH(row); row.header.setAlpha(mExpanded && isActive ? 1 : 0.5f); updateVolumeRowSliderTintH(row, isActive); } }
遍历已经添加的音量类型集合mRows,依次判断是否处于活动状态,再和开始设置的important属性比较。mExpanded是否展开,默认只显示铃声音量控制,点击下拉的按钮,才完全显示其它的音量控制
mExpanded && row.view.getVisibility() == View.VISIBLE || (mExpanded && (row.important || isActive)) || !mExpanded && isActive
true && false || (true && (true || false)) || false && true —>true
好了,至此分析完毕,重新mmm push SystemUI.apk 查看效果
第二个功能
源码位置
Settings\res_ext\xml\edit_profile_prefs.xml
Settings\src\com\mediatek\audioprofile\Editprofile.java
Settings\src\com\mediatek\audioprofile\VolumeSeekBarPreference.java
在edit_profile_prefs.xml中仿照原来的Alarm volume和Ring volume,新增加一个Call volume
<!-- Media volume --> <com.mediatek.audioprofile.VolumeSeekBarPreference android:key="media_volume" android:icon="@*android:drawable/ic_audio_vol" android:title="@string/media_volume_option_title" /> <!-- Alarm volume --> <com.mediatek.audioprofile.VolumeSeekBarPreference android:key="alarm_volume" android:icon="@*android:drawable/ic_audio_alarm" android:title="@string/alarm_volume_option_title" /> <!-- Ring volume --> <com.mediatek.audioprofile.VolumeSeekBarPreference android:key="ring_volume" android:icon="@*android:drawable/ic_audio_ring_notif" android:title="@string/ring_volume_option_title" /> <!-- Call volume --> <com.mediatek.audioprofile.VolumeSeekBarPreference android:key="call_volume" android:icon="@drawable/ic_volume_voice" android:title="@string/call_volume_option_title" />
对应的drawable文件时从SystemUI中拷贝过来的,ic_volume_voice.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24.0dp" android:viewportHeight="48.0" android:viewportWidth="48.0" android:width="24.0dp" > <path android:fillColor="#ff727272" android:pathData="M13.25,21.59c2.88,5.66 7.51,10.29 13.18,13.17l4.4,-4.41c0.55,-0.55 1.34,-0.71 2.03,-0.49C35.1,30.6 37.51,31.0 40.0,31.0c1.11,0.0 2.0,0.89 2.0,2.0l0.0,7.0c0.0,1.11 -0.89,2.0 -2.0,2.0C21.22,42.0 6.0,26.78 6.0,8.0c0.0,-1.1 0.9,-2.0 2.0,-2.0l7.0,0.0c1.11,0.0 2.0,0.89 2.0,2.0 0.0,2.4 0.4,4.9 1.14,7.1 0.2,0.6 0.06,1.48 -0.49,2.03l-4.4,4.42z" /> </vector>
接下来对应到 Editprofile.java 文件中,可以看到 KEY_ALARM_VOLUME 对应的preference初始化,依旧照葫芦画瓢,添加 KEY_CALL_VOLUME
private void initVolume(PreferenceScreen parent) { initVolumePreference(KEY_MEDIA_VOLUME, AudioManager.STREAM_MUSIC); initVolumePreference(KEY_ALARM_VOLUME, AudioManager.STREAM_ALARM); initVolumePreference(KEY_CALL_VOLUME, AudioManager.STREAM_VOICE_CALL); if (mVoiceCapable) { mVolume = initVolumePreference(KEY_RING_VOLUME, AudioManager.STREAM_RING); parent.removePreference(parent.findPreference(KEY_NOTIFICATION_VOLUME)); } else { mVolume = initVolumePreference(KEY_NOTIFICATION_VOLUME, AudioManager.STREAM_NOTIFICATION); parent.removePreference(parent.findPreference(KEY_RING_VOLUME)); } }
重新编译,push替换后发现,UI倒是出来了,但是无法滑动,事情果然没那么简单,继续查看 initVolumePreference()
private VolumeSeekBarPreference initVolumePreference(String key, int stream) { Log.d("@M_" + TAG, "Init volume preference, key = " + key + ",stream = " + stream); final VolumeSeekBarPreference volumePref = (VolumeSeekBarPreference) findPreference(key); volumePref.setStream(stream); volumePref.setCallback(mVolumeCallback); volumePref.setProfile(mKey); return volumePref; }
保存了当前的音量调节类型,设置seekBar回调事件,接下来看看回调处理了什么
private final class VolumePreferenceCallback implements VolumeSeekBarPreference.Callback { private SeekBarVolumizer mCurrent; @Override public void onSampleStarting(SeekBarVolumizer sbv) { if (mCurrent != null && mCurrent != sbv) { mCurrent.stopSample(); } mCurrent = sbv; if (mCurrent != null) { mHandler.removeMessages(H.STOP_SAMPLE); mHandler.sendEmptyMessageDelayed(H.STOP_SAMPLE, SAMPLE_CUTOFF); } } public void onStreamValueChanged(int stream, int progress) { if (stream == AudioManager.STREAM_RING) { mHandler.removeMessages(H.UPDATE_RINGER_ICON); mHandler.obtainMessage(H.UPDATE_RINGER_ICON, progress, 0).sendToTarget(); } } public void stopSample() { if (mCurrent != null) { mCurrent.stopSample(); } } public void ringtoneChanged() { if (mCurrent != null) { mCurrent.ringtoneChanged(); } else { mVolume.getSeekBar().ringtoneChanged(); } } }
当我们点击或者是滑动seekBar时,会根据当前设置的音量大小播放一段短暂的默认铃音,当铃音未播放完成时,再次点击将不进行播放。继续跟进 VolumeSeekBarPreference.java 中
@Override protected void onBindView(View view) { super.onBindView(view); if (mStream == 0) { Log.w(TAG, "No stream found, not binding volumizer "); return; } getPreferenceManager().registerOnActivityStopListener(this); final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); if (seekBar == mSeekBar) { return; } mSeekBar = seekBar; final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() { @Override public void onSampleStarting(SeekBarVolumizer sbv) { if (mCallback != null) { mCallback.onSampleStarting(sbv); } } }; final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null; if (mVolumizer == null) { mVolumizer = new SeekBarVolumizer(getContext(), mStream, sampleUri, sbvc, mKey); } //mVolumizer.setProfile(mKey); mVolumizer.setSeekBar(mSeekBar); }
mStream == 0, 直接return,会不会和这有关系呢,来看下各个音量调节类型对应的int值
/** Used to identify the volume of audio streams for phone calls */ public static final int STREAM_VOICE_CALL = 0; /** Used to identify the volume of audio streams for the phone ring and message alerts */ public static final int STREAM_RING = 2; /** Used to identify the volume of audio streams for music playback */ public static final int STREAM_MUSIC = 3; /** Used to identify the volume of audio streams for alarms */ public static final int STREAM_ALARM = 4;
我们新增的 STREAM_VOICE_CALL对应的mStream正好为0,直接给return掉了,所以修改为 mStream < 0 即可,重新编译 push 发现成功了。
具体的音量调节逻辑在 packages\apps\Settings\src\com\mediatek\audioprofile\SeekBarVolumizer.java 中,感兴趣的可继续深究,肯定离不开调用 .setStreamVolume()方法,大概看了一眼,主要是onProgressChanged()回调,通过postSetVolume(progress)方法,发送MSG_SET_STREAM_VOLUME消息,最终调用saveVolume()