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()