1.概述
根据设置主界面加载流程,我们知道设置的二级/三级界面大部分启动的都是SubSettings,在SubSettings中加载不同的Fragment,在一级菜单top_level_settings.xml中,可以确定“声音”菜单启动的是"com.android.settings.notification.SoundSettings"这个fragment。
sound_settings.xml
<Preference
android:key="top_level_sound"
android:title="@string/sound_settings"
android:summary="@string/sound_dashboard_summary"
android:icon="@drawable/ic_homepage_sound"
android:order="-70"
android:fragment="com.android.settings.notification.SoundSettings"/>
SoundSettings中,加载的是sound_settings.xml,这里面定义了“声音界面”的全部子菜单。
@Override
protected int getPreferenceScreenResId() {
return R.xml.sound_settings;
}
首先看下sound_settings的菜单界面和具体内容:
<PreferenceScreen
xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
xmlns:settings="[http://schemas.android.com/apk/res-auto](http://schemas.android.com/apk/res-auto)"
android:title="@string/sound_settings"
android:key="sound_settings"
settings:keywords="@string/keywords_sounds"
settings:initialExpandedChildrenCount="9">
......
<!-- 媒体音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="media_volume"
android:icon="@drawable/ic_media_stream"
android:title="@string/media_volume_option_title"
android:order="-180"
settings:controller="com.android.settings.notification.MediaVolumePreferenceController"/>
......
<!-- 通话音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="call_volume"
android:icon="@drawable/ic_local_phone_24_lib"
android:title="@string/call_volume_option_title"
android:order="-170"
settings:controller="com.android.settings.notification.CallVolumePreferenceController"/>
......
<!-- 铃声和通知音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="ring_volume"
android:icon="@drawable/ic_notifications"
android:title="@string/ring_volume_option_title"
android:order="-160"
settings:controller="com.android.settings.notification.RingVolumePreferenceController"/>
<!-- 闹钟音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="alarm_volume"
android:icon="@*android:drawable/ic_audio_alarm"
android:title="@string/alarm_volume_option_title"
android:order="-150"
settings:controller="com.android.settings.notification.AlarmVolumePreferenceController"/>
<!-- 通知音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="notification_volume"
android:icon="@drawable/ic_notifications"
android:title="@string/notification_volume_option_title"
android:order="-140"
settings:controller="com.android.settings.notification.NotificationVolumePreferenceController"/>
<!-- 来电振动 -->
<Preference
android:fragment="com.android.settings.sound.VibrateForCallsPreferenceFragment"
android:key="vibrate_for_calls"
android:title="@string/vibrate_when_ringing_title"
android:order="-130"
settings:controller="com.android.settings.sound.VibrateForCallsPreferenceController"
settings:keywords="@string/keywords_vibrate_for_calls"/>
<!-- 勿扰模式 -->
<com.android.settingslib.RestrictedPreference
android:key="zen_mode"
android:title="@string/zen_mode_settings_title"
android:fragment="com.android.settings.notification.zen.ZenModeSettings"
android:order="-120"
settings:useAdminDisabledSummary="true"
settings:keywords="@string/keywords_sounds_and_notifications_interruptions"
settings:allowDividerAbove="true"
settings:controller="com.android.settings.notification.zen.ZenModePreferenceController"/>
<!-- 媒体 -->
<Preference
android:key="media_controls_summary"
android:title="@string/media_controls_title"
android:fragment="com.android.settings.sound.MediaControlsSettings"
android:order="-110"
settings:controller="com.android.settings.sound.MediaControlsParentPreferenceController"
settings:keywords="@string/keywords_media_controls"/>
<!-- 阻止响铃的快捷方式 -->
<Preference
android:key="gesture_prevent_ringing_sound"
android:title="@string/gesture_prevent_ringing_sound_title"
android:order="-107"
android:fragment="com.android.settings.gestures.PreventRingingGestureSettings"
settings:controller="com.android.settings.gestures.PreventRingingParentPreferenceController"/>
<!-- 手机铃声 -->
<com.android.settings.DefaultRingtonePreference
android:key="phone_ringtone"
android:title="@string/ringtone_title"
android:dialogTitle="@string/ringtone_title"
android:summary="@string/summary_placeholder"
android:ringtoneType="ringtone"
android:order="-100"
settings:keywords="@string/sound_settings"/>
<!-- 默认通知提示音 -->
<com.android.settings.DefaultRingtonePreference
android:key="notification_ringtone"
android:title="@string/notification_ringtone_title"
android:dialogTitle="@string/notification_ringtone_title"
android:summary="@string/summary_placeholder"
android:ringtoneType="notification"
android:order="-90"/>
<!-- 默认闹钟提示音 -->
<com.android.settings.DefaultRingtonePreference
android:key="alarm_ringtone"
android:title="@string/alarm_ringtone_title"
android:dialogTitle="@string/alarm_ringtone_title"
android:summary="@string/summary_placeholder"
android:persistent="false"
android:ringtoneType="alarm"
android:order="-80"/>
......
</PreferenceScreen>
注:只列出主要部分
通过上面的分析,我们将声音的菜单拆解成三个子模块,来详细讲解其工作原理,分别是:
- 音量模块
- 勿扰模式
- 默认铃声设置
2.音量模块详解
2.1音量模块代码架构和初始化流程
首先音量条都是使用VolumeSeekBarPreference,VolumeSeekBarPreference继承于SeekBarPreference,不同的音量条主要是通过不同的controller来控制其音量显示和调节。由于每个音量条的代码逻辑和结构都大致相同,我们就以铃声和通知音量为例,研究一下其具体工作原理:
<!-- 铃声和通知音量 -->
<com.android.settings.notification.VolumeSeekBarPreference
android:key="ring_volume"
android:icon="@drawable/ic_notifications"
android:title="@string/ring_volume_option_title"
android:order="-160"
settings:controller="com.android.settings.notification.RingVolumePreferenceController"/>
从sound_settings的菜单界面可以看到,这里有四个音量条,但是sound_settings.xml却定义了五个VolumeSeekBarPreference,这是为什么呢?这个我们后边给出答案。
ring_volume使用的controller是RingVolumePreferenceController,它继承于VolumeSeekBarPreferenceController,重写其getPreferenceKey() 、getAvailabilityStatus() 、getAudioStream()。
public class RingVolumePreferenceController extends VolumeSeekBarPreferenceController {
private static final String TAG = "RingVolumeController";
private static final String KEY_RING_VOLUME = "ring_volume";
......
@Override
public String getPreferenceKey() {
return KEY_RING_VOLUME;
}
@Override
public int getAvailabilityStatus() {
return Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume()
? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
@Override
public int getAudioStream() {
return AudioManager.STREAM_RING;
}
......
}
getAvailabilityStatus()方法获取当前preference可用性状态,铃声和通知音量这里调用isVoiceCapable()方法,查看其实现逻辑和方法,我们发现这个方法返回当前是被是否具有语音能力(即可以通话)。
/**
* Returns whether the device is voice-capable (meaning, it is also a phone).
*/
public static boolean isVoiceCapable(Context context) {
final TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
return telephony != null && telephony.isVoiceCapable();
}
我们再查看NotificationVolumePreferenceController的getAvailabilityStatus()方法,发现它正好和RingVolumePreferenceController 相反。也就是说,在具有语音能力的设备(如手机)上显示RingVolumePreference(同时兼具调节铃声和通知声音的功能),在不具有语音能力的设备(如平板、电视)上显示NotificationVolumePreference(只能调节通知声音)。这就解释了我们前面提到的界面上有四个音量条,但是sound_settings.xml却定义了五个的问题。
@Override
public int getAvailabilityStatus() {
return mContext.getResources().getBoolean(R.bool.config_show_notification_volume)
&& !Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume()
? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
我们继续来看RingVolumePreferenceController的父类,displayPreference()时调用了mPreference.setStream(getAudioStream()),getAudioStream从子类中获取,不同的声音类型对应的AudioStream不同:
- 媒体音量:AudioManager.STREAM_MUSIC;
- 通话音量:AudioManager.STREAM_VOCIE_CALL;
- 铃声和通知音量:AudioManager.STREAM_RING;
- 闹钟音量:AudioManager.STREAM_ALARM;
- 通知音量:AudioManager.STREAM_NOTIFICATION;
可能有人会有疑问,铃声和通知音量设置的是STREAM_RING,并没有设置STREAM_NOTIFICATION,为什么可以控制通知音量,这是因为AudioManager在底层也有处理,在具有语音能力的设备上,通知的音量设置使用的就是铃声的流(STREAM_RING),具体这里我们不展开讲,有兴趣的同学可以去查看AudioManager的代码。
/**
* Base class for preference controller that handles VolumeSeekBarPreference
*/
public abstract class VolumeSeekBarPreferenceController extends
AdjustVolumeRestrictedPreferenceController implements LifecycleObserver {
protected VolumeSeekBarPreference mPreference;
protected VolumeSeekBarPreference.Callback mVolumePreferenceCallback;
protected AudioHelper mHelper;
public VolumeSeekBarPreferenceController(Context context, String key) {
super(context, key);
setAudioHelper(new AudioHelper(context));
}
@VisibleForTesting
void setAudioHelper(AudioHelper helper) {
mHelper = helper;
}
public void setCallback(Callback callback) {
mVolumePreferenceCallback = callback;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
if (isAvailable()) {
mPreference = screen.findPreference(getPreferenceKey());
mPreference.setCallback(mVolumePreferenceCallback);
mPreference.setStream(getAudioStream());
mPreference.setMuteIcon(getMuteIcon());
}
}
@Override
public int getSliderPosition() {
if (mPreference != null) {
return mPreference.getProgress();
}
return mHelper.getStreamVolume(getAudioStream());
}
@Override
public boolean setSliderPosition(int position) {
if (mPreference != null) {
mPreference.setProgress(position);
}
return mHelper.setStreamVolume(getAudioStream(), position);
}
@Override
public int getMax() {
if (mPreference != null) {
return mPreference.getMax();
}
return mHelper.getMaxVolume(getAudioStream());
}
@Override
public int getMin() {
if (mPreference != null) {
return mPreference.getMin();
}
return mHelper.getMinVolume(getAudioStream());
}
public abstract int getAudioStream();
}
VolumeSeekBarPreference setStream()方法调用父类的setMax() setMin() setProgress()三个方法和设置SeekBar的最大值、最小值和进度值。
/** A slider preference that directly controls an audio stream volume (no dialog) **/
public class VolumeSeekBarPreference extends SeekBarPreference {
private static final String TAG = "VolumeSeekBarPreference";
protected SeekBar mSeekBar;
private int mStream;
private SeekBarVolumizer mVolumizer;
private Callback mCallback;
private ImageView mIconView;
private TextView mSuppressionTextView;
private String mSuppressionText;
private boolean mMuted;
private boolean mZenMuted;
private int mIconResId;
private int mMuteIconResId;
private boolean mStopped;
@VisibleForTesting
AudioManager mAudioManager;
public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLayoutResource(R.layout.preference_volume_slider);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
......
public void setStream(int stream) {
mStream = stream;
setMax(mAudioManager.getStreamMaxVolume(mStream));
// Use getStreamMinVolumeInt for non-public stream type
// eg: AudioManager.STREAM_BLUETOOTH_SCO
setMin(mAudioManager.getStreamMinVolumeInt(mStream));
setProgress(mAudioManager.getStreamVolume(mStream));
}
public void setCallback(Callback callback) {
mCallback = callback;
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar);
mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
mSuppressionTextView = (TextView) view.findViewById(R.id.suppression_text);
init();
}
protected void init() {
if (mSeekBar == null) return;
final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() {
@Override
public void onSampleStarting(SeekBarVolumizer sbv) {
if (mCallback != null) {
mCallback.onSampleStarting(sbv);
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
if (mCallback != null) {
mCallback.onStreamValueChanged(mStream, progress);
}
}
@Override
public void onMuted(boolean muted, boolean zenMuted) {
if (mMuted == muted && mZenMuted == zenMuted) return;
mMuted = muted;
mZenMuted = zenMuted;
updateIconView();
}
};
final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
if (mVolumizer == null) {
mVolumizer = new SeekBarVolumizer(getContext(), mStream, sampleUri, sbvc);
}
mVolumizer.start();
mVolumizer.setSeekBar(mSeekBar);
updateIconView();
updateSuppressionText();
if (!isEnabled()) {
mSeekBar.setEnabled(false);
mVolumizer.stop();
}
}
protected void updateIconView() {
if (mIconView == null) return;
if (mIconResId != 0) {
mIconView.setImageResource(mIconResId);
} else if (mMuteIconResId != 0 && mMuted && !mZenMuted) {
mIconView.setImageResource(mMuteIconResId);
} else {
mIconView.setImageDrawable(getIcon());
}
}
public void showIcon(int resId) {
// Instead of using setIcon, which will trigger listeners, this just decorates the
// preference temporarily with a new icon.
if (mIconResId == resId) return;
mIconResId = resId;
updateIconView();
}
public void setMuteIcon(int resId) {
if (mMuteIconResId == resId) return;
mMuteIconResId = resId;
updateIconView();
}
private Uri getMediaVolumeUri() {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ getContext().getPackageName()
+ "/" + R.raw.media_volume);
}
public void setSuppressionText(String text) {
if (Objects.equals(text, mSuppressionText)) return;
mSuppressionText = text;
updateSuppressionText();
}
protected void updateSuppressionText() {
if (mSuppressionTextView != null && mSeekBar != null) {
mSuppressionTextView.setText(mSuppressionText);
final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText);
mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.GONE);
}
}
public interface Callback {
void onSampleStarting(SeekBarVolumizer sbv);
void onStreamValueChanged(int stream, int progress);
}
}
setMax() setMin() setProgress() 方法会分别设置全局变量mMax、mMin、mProgress参数,然后执行notifyChanged()方法,经过系统调用执行到Preference的onBindViewHolder()方法,进行数据和视图的绑定。
/**
* Based on android.preference.SeekBarPreference, but uses support preference as base.
*/
public class SeekBarPreference extends RestrictedPreference
implements OnSeekBarChangeListener, View.OnKeyListener {
private int mProgress;
private int mMax;
private int mMin;
private boolean mTrackingTouch;
private boolean mContinuousUpdates;
private int mDefaultProgress = -1;
private SeekBar mSeekBar;
private boolean mShouldBlink;
private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT;
private CharSequence mSeekBarContentDescription;
public SeekBarPreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes);
setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax));
setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin));
a.recycle();
a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
final int layoutResId = a.getResourceId(
com.android.internal.R.styleable.SeekBarPreference_layout,
com.android.internal.R.layout.preference_widget_seekbar);
a.recycle();
a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
final boolean isSelectable = a.getBoolean(
com.android.settings.R.styleable.Preference_android_selectable, false);
setSelectable(isSelectable);
a.recycle();
setLayoutResource(layoutResId);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
view.itemView.setOnKeyListener(this);
mSeekBar = (SeekBar) view.findViewById(
com.android.internal.R.id.seekbar);
mSeekBar.setOnSeekBarChangeListener(this);
mSeekBar.setMax(mMax);
mSeekBar.setMin(mMin);
mSeekBar.setProgress(mProgress);
mSeekBar.setEnabled(isEnabled());
final CharSequence title = getTitle();
if (!TextUtils.isEmpty(mSeekBarContentDescription)) {
mSeekBar.setContentDescription(mSeekBarContentDescription);
} else if (!TextUtils.isEmpty(title)) {
mSeekBar.setContentDescription(title);
}
......
}
......
public void setMax(int max) {
if (max != mMax) {
mMax = max;
notifyChanged();
}
}
public void setMin(int min) {
if (min != mMin) {
mMin = min;
notifyChanged();
}
}
public void setProgress(int progress) {
setProgress(progress, true);
}
......
private void setProgress(int progress, boolean notifyChanged) {
if (progress > mMax) {
progress = mMax;
}
if (progress < mMin) {
progress = mMin;
}
if (progress != mProgress) {
mProgress = progress;
persistInt(progress);
if (notifyChanged) {
notifyChanged();
}
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && (mContinuousUpdates || !mTrackingTouch)) {
syncProgress(seekBar);
}
}
......
}
2.2音量设置流程
音量条加载完成之后,我们可以对音量条seekbar进行拖拽和点击以设置音量,接下来我们研究音量的设置流程。
首先我们知道SeekBar是在VolumeSeekBarPreference中的onBindViewHolder中通过init()初始化的,这里借助SeekBarVolumizer对声音进行控制。
首先创建SeekBarVolumizer对象,这里注意参数,mStream, sampleUri, sbvc分别代表声音类型、样例声音的uri、和毁掉方法。然后调用start()方法开始监听底层音量变化,再调用setSeekBar()方法将mSeekBar传递给SeekBarVolumizer。
protected void init() {
if (mSeekBar == null) return;
final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() {
@Override
public void onSampleStarting(SeekBarVolumizer sbv) {
if (mCallback != null) {
mCallback.onSampleStarting(sbv);
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
if (mCallback != null) {
mCallback.onStreamValueChanged(mStream, progress);
}
}
@Override
public void onMuted(boolean muted, boolean zenMuted) {
if (mMuted == muted && mZenMuted == zenMuted) return;
mMuted = muted;
mZenMuted = zenMuted;
updateIconView();
}
};
final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
if (mVolumizer == null) {
mVolumizer = new SeekBarVolumizer(getContext(), mStream, sampleUri, sbvc);
}
mVolumizer.start();
mVolumizer.setSeekBar(mSeekBar);
updateIconView();
updateSuppressionText();
if (!isEnabled()) {
mSeekBar.setEnabled(false);
mVolumizer.stop();
}
}
我们继续重点看下setSeekBar()方法,setSeekBar()首先调用updateSeekBar(),设置SeekBar的进度。之后调用了setOnSeekBarChangeListener(this)方法,也就是说,mVolumizer获得了监控SeekBar变化的能力。
public void setSeekBar(SeekBar seekBar) {
if (mSeekBar != null) {
mSeekBar.setOnSeekBarChangeListener(null);
}
mSeekBar = seekBar;
mSeekBar.setOnSeekBarChangeListener(null);
mSeekBar.setMax(mMaxStreamVolume);
updateSeekBar();
mSeekBar.setOnSeekBarChangeListener(this);
}
protected void updateSeekBar() {
final boolean zenMuted = isZenMuted();
mSeekBar.setEnabled(!zenMuted);
if (zenMuted) {
mSeekBar.setProgress(mLastAudibleStreamVolume);
} else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
mSeekBar.setProgress(0);
} else if (mMuted) {
mSeekBar.setProgress(0);
} else {
mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume);
}
}
我们继续来看SeekBar变化之后的逻辑。当SeekBar进度发生变化时,会回调onProgressChanged方法,fromTouch代表用户主动点击,所以此处判断fromTouch为true时,才会真正设置音量,走到postSetVolume()方法。postSetVolume()方法中通过mHandler将消息传递到handleMessage()方法,进行实际的音量设置。
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
if (fromTouch) {
postSetVolume(progress);
}
if (mCallback != null) {
mCallback.onProgressChanged(seekBar, progress, fromTouch);
}
}
private void postSetVolume(int progress) {
if (mHandler == null) return;
// Do the volume changing separately to give responsive UI
mLastProgress = progress;
mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_SET_STREAM_VOLUME:
if (mMuted && mLastProgress > 0) {
mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_UNMUTE, 0);
} else if (!mMuted && mLastProgress == 0) {
mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_MUTE, 0);
}
mAudioManager.setStreamVolume(mStreamType, mLastProgress,
AudioManager.FLAG_SHOW_UI_WARNINGS);
break;
......
}
return true;
}
2.3音量被动调节流程
在2.2我们了解了用户调整进度条到设置底层音量的流程,那么底层音量改变之后,到上层进度条变化之间的流程是怎样的,在2.3我们来详细了解一下。
在上一节中我们讲到,VolumeSeekBarPreference中执行init()方法进行初始化,init()方法调用
mVolumizer.start()监听底层音量变化,我们来看一下start()方法中都做了些什么。
可以看到,这里通过registerContentObserver将mVolumeObserver注册到ContentResolver中,然后调用了mReceiver.setListening(true)方法。
public void start() {
if (mHandler != null) return; // already started
HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
thread.start();
mHandler = new Handler(thread.getLooper(), this);
mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
mVolumeObserver = new Observer(mHandler);
mContext.getContentResolver().registerContentObserver(
System.getUriFor(System.VOLUME_SETTINGS_INT[mStreamType]),
false, mVolumeObserver);
mReceiver.setListening(true);
if (hasAudioProductStrategies()) {
registerVolumeGroupCb();
}
}
registerContentObserver方法的回调如下,但实际上这个回调方法在音量变化的时候并没有执行,不清楚是方法不对还是此方法已经废弃了。
private final class Observer extends ContentObserver {
public Observer(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
updateSlider();
}
}
而真正的回调是通过广播(mReceiver.setListening(true))来监听的,setListening()方法中会注册广播接收器,监听了5个action,AudioManager.VOLUME_CHANGED_ACTION是音量变化action。当音量发生变化时,会发送广播,并且携带声音类型(EXTRA_VOLUME_STREAM_VALUE)和音量值(EXTRA_VOLUME_STREAM_VALUE)参数,随后调用updateVolumeSlider()方法更新SeekBar的进度。
private final class Receiver extends BroadcastReceiver {
private boolean mListening;
public void setListening(boolean listening) {
if (mListening == listening) return;
mListening = listening;
if (listening) {
final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
filter.addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED);
filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
mContext.registerReceiver(this, filter);
} else {
mContext.unregisterReceiver(this);
}
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
if (hasAudioProductStrategies()) {
updateVolumeSlider(streamType, streamValue);
}
}
......
}
}
updateVolumeSlider()方法判断判断广播携带的stream类型是否和当前SeekBarPreference的stream类型相等,相等(streamMatch为true)才会发送更新进度条的消息。需要注意的是mNotification和Ring 类型是可以互相匹配的。
private void updateVolumeSlider(int streamType, int streamValue) {
final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
: (streamType == mStreamType);
if (mSeekBar != null && streamMatch && streamValue != -1) {
final boolean muted = mAudioManager.isStreamMute(mStreamType)
|| streamValue == 0;
mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
}
}
3.勿扰模式详解
勿扰模式菜单定义如下,ZenModePreferenceController用来控制菜单上的显示内容,ZenModeSettings是点击菜单跳转的界面,我们直接来看ZenModeSettings。
!-- 勿扰模式 -->
<com.android.settingslib.RestrictedPreference
android:key="zen_mode"
android:title="@string/zen_mode_settings_title"
android:fragment="com.android.settings.notification.zen.ZenModeSettings"
android:order="-120"
settings:useAdminDisabledSummary="true"
settings:keywords="@string/keywords_sounds_and_notifications_interruptions"
settings:allowDividerAbove="true"
settings:controller="com.android.settings.notification.zen.ZenModePreferenceController"/>
ZenModeSettings加载的配置文件是zen_mode_settings.xml,内容如下:
zen_mode_toggle是勿扰模式开关;zen_mode_behavior_people,zen_mode_behavior_apps,zen_sound_vibration_settings是勿扰模式的配置项,分别配置例外的联系人、例外的应用以及闹钟和其它例外项;zen_mode_automation_settings对应时间表菜单,用来配置勿扰模式的自动开启和关闭;zen_mode_settings_advanced是高级设置,包括勿扰模式持续时间和隐藏通知的显示方式。
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/zen_mode_settings_title">
<!-- Turn on DND button -->
<com.android.settingslib.widget.LayoutPreference
android:key="zen_mode_toggle"
android:title="@string/zen_mode_settings_title"
android:selectable="false"
android:layout="@layout/zen_mode_settings_button"
settings:allowDividerBelow="true"
settings:keywords="@string/keywords_zen_mode_settings"/>
<PreferenceCategory
android:key="zen_mode_settings_category_behavior"
android:title="@string/zen_category_behavior">
<!-- People -->
<Preference
android:key="zen_mode_behavior_people"
android:title="@string/zen_category_people"
android:fragment="com.android.settings.notification.zen.ZenModePeopleSettings" />
<!-- Apps -->
<Preference
android:key="zen_mode_behavior_apps"
android:title="@string/zen_category_apps"
android:fragment="com.android.settings.notification.zen.ZenModeBypassingAppsSettings" />
<!-- All sounds -->
<Preference
android:key="zen_sound_vibration_settings"
android:title="@string/zen_category_exceptions"
android:fragment="com.android.settings.notification.zen.ZenModeSoundVibrationSettings" />
</PreferenceCategory>
<!-- Automatic rules -->
<Preference
android:key="zen_mode_automation_settings"
android:title="@string/zen_category_schedule"
settings:allowDividerAbove="true"
android:fragment="com.android.settings.notification.zen.ZenModeAutomationSettings"/>
<PreferenceCategory
android:key="zen_mode_settings_advanced"
settings:initialExpandedChildrenCount="0">
<!-- DND duration settings -->
<com.android.settings.notification.zen.ZenDurationDialogPreference
android:key="zen_mode_duration_settings"
android:title="@string/zen_category_duration"
android:widgetLayout="@null"/>
<!-- What to block (effects) -->
<Preference
android:key="zen_mode_block_effects_settings"
android:title="@string/zen_mode_restrict_notifications_title"
android:fragment="com.android.settings.notification.zen.ZenModeRestrictNotificationsSettings" />
</PreferenceCategory>
<!-- Footer that shows if user is put into alarms only or total silence mode by an app -->
<com.android.settingslib.widget.FooterPreference/>
</PreferenceScreen>
3.1勿扰模式开关
我们首先从勿扰模式的开关入手分析勿扰模式的流程。
点击勿扰模式开关时,执行到mZenButtonOn的OnClickListener方法中,这里通过zenDuration执行不同的代码分支。zenDuration就是“在快捷设置中开启的持续时长”菜单中设定的持续时间,有“直到关闭”、“1小时(可自行设置)”、“每次都询问”三个选项。
这里不同的分支最终都指向mBackend的两个方法 setZenMode() 和 setZenModeForDuration()。
vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/notification/zen/ZenModeButtonPreferenceController.java
private void updateZenButtonOnClickListener(Preference preference) {
mZenButtonOn.setOnClickListener(v -> {
mRefocusButton = true;
writeMetrics(preference, true);
int zenDuration = getZenDuration();
switch (zenDuration) {
case Settings.Secure.ZEN_DURATION_PROMPT:
new SettingsEnableZenModeDialog().show(mFragment, TAG);
break;
case Settings.Secure.ZEN_DURATION_FOREVER:
mBackend.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
break;
default:
mBackend.setZenModeForDuration(zenDuration);
}
});
}
从下面的代码中可以看到,setZenMode()方法和setZenModeForDuration()最终都会调用mNotificationManager.setZenMode方法,不同的是参数,如果有时间限制,这里会将时间转化为uri->conditionId,往下传递。
setZenMode()方法还有另外一个参数zenMode,它有如下可能的值:
public static final int ZEN_MODE_OFF = 0;//关闭勿扰模式
public static final int ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1;//开启勿扰模式(根据勿扰配置禁止铃声)
public static final int ZEN_MODE_NO_INTERRUPTIONS = 2;//开启勿扰模式,禁止所有铃声
public static final int ZEN_MODE_ALARMS = 3;//开启勿扰模式,仅alarm可以响铃
vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/notification/zen/ZenModeBackend.java
protected void setZenMode(int zenMode) {
NotificationManager.from(mContext).setZenMode(zenMode, null, TAG);
mZenMode = getZenMode();
}
protected void setZenModeForDuration(int minutes) {
Uri conditionId = ZenModeConfig.toTimeCondition(mContext, minutes,
ActivityManager.getCurrentUser(), true).id;
mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
conditionId, TAG);
mZenMode = getZenMode();
}
这里继续通过binder调用来到NotificationManagerService,调用setZenMode方法。NotificationManagerService本身不处理勿扰模式的相关逻辑,它将所有有关勿扰的事情都交给mZenModeHelper处理,这里继续代用mZenModeHelper 的 setManualZenMode() 方法。
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
@Override
public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException {
enforceSystemOrSystemUI("INotificationManager.setZenMode");
final long identity = Binder.clearCallingIdentity();
try {
mZenModeHelper.setManualZenMode(mode, conditionId, null, reason);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
我们继续进入ZenModeHelper中查看,这里首先创建了一个ZenModeConfig->newConfig,后续有关勿扰模式的所有配置都会存储在ZenModeConfig中,我们后边也会解释它的数据结构。然后判断zenMode是否是关闭状态,如果是关闭状态manualRule设置为空,automaticRule设置为停止工作状态;如果是打开状态,则创建一个新的ZenRule,并赋值给manualRule。manualRule和automaticRule都是ZenModeConfig的数据结构ZenRule,后面我们会详细解释,这里先暂且将它理解为勿扰模式的规则。这一步主要是初始化ZenModeConfig,接着继续调用setConfigLocked()设置ZenModeConfig。
frameworks/base/services/core/java/com/android/server/notification/ZenModeHelper.java
private void setManualZenMode(int zenMode, Uri conditionId, String reason, String caller,
boolean setRingerMode) {
ZenModeConfig newConfig;
synchronized (mConfig) {
if (mConfig == null) return;
if (!Global.isValidZenMode(zenMode)) return;
if (DEBUG) Log.d(TAG, "setManualZenMode " + Global.zenModeToString(zenMode)
+ " conditionId=" + conditionId + " reason=" + reason
+ " setRingerMode=" + setRingerMode);
newConfig = mConfig.copy();
if (zenMode == Global.ZEN_MODE_OFF) {
newConfig.manualRule = null;
for (ZenRule automaticRule : newConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
automaticRule.snoozing = true;
}
}
} else {
final ZenRule newRule = new ZenRule();
newRule.enabled = true;
newRule.zenMode = zenMode;
newRule.conditionId = conditionId;
newRule.enabler = caller;
newConfig.manualRule = newRule;
}
setConfigLocked(newConfig, reason, null, setRingerMode);
}
}
setConfigLocked代码很长,但其实只是做了一些判断和通知的工作,最后通过Handler发送消息调用到applyConfig()这个方法。
private boolean setConfigLocked(ZenModeConfig config, String reason,
ComponentName triggeringComponent, boolean setRingerMode) {
final long identity = Binder.clearCallingIdentity();
try {
if (config == null || !config.isValid()) {
Log.w(TAG, "Invalid config in setConfigLocked; " + config);
return false;
}
if (config.user != mUser) {
// simply store away for background users
mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
return true;
}
// handle CPS backed conditions - danger! may modify config
mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
ZenLog.traceConfig(reason, mConfig, config);
// send some broadcasts
final boolean policyChanged = !Objects.equals(getNotificationPolicy(mConfig),
getNotificationPolicy(config));
if (!config.equals(mConfig)) {
dispatchOnConfigChanged();
updateConsolidatedPolicy(reason);
}
if (policyChanged) {
dispatchOnPolicyChanged();
}
mConfig = config;
mHandler.postApplyConfig(config, reason, triggeringComponent, setRingerMode);
return true;
} catch (SecurityException e) {
Log.wtf(TAG, "Invalid rule in config", e);
return false;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private void postApplyConfig(ZenModeConfig config, String reason,
ComponentName triggeringComponent, boolean setRingerMode) {
sendMessage(obtainMessage(MSG_APPLY_CONFIG,
new ConfigMessageData(config, reason, triggeringComponent, setRingerMode)));
}
Override
public void handleMessage(Message msg) {
switch (msg.what) {
......
case MSG_APPLY_CONFIG:
ConfigMessageData applyConfigData = (ConfigMessageData) msg.obj;
applyConfig(applyConfigData.config, applyConfigData.reason,
applyConfigData.triggeringComponent, applyConfigData.setRingerMode);
.....
}
}
applyConfig()方法又通过evaluateZenMode()方法应用勿扰模式的配置,具体的步骤在代码中注释。
private void applyConfig(ZenModeConfig config, String reason,
ComponentName triggeringComponent, boolean setRingerMode) {
final String val = Integer.toString(config.hashCode());
Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);
//应用勿扰模式的配置
evaluateZenMode(reason, setRingerMode);
//设置勿扰模式的时间段
mConditions.evaluateConfig(config, triggeringComponent, true /*processSubscriptions*/);
}
@VisibleForTesting
protected void evaluateZenMode(String reason, boolean setRingerMode) {
if (DEBUG) Log.d(TAG, "evaluateZenMode");
if (mConfig == null) return;
final int policyHashBefore = mConsolidatedPolicy == null ? 0
: mConsolidatedPolicy.hashCode();
final int zenBefore = mZenMode;
//通过计算获得勿扰模式的状态
final int zen = computeZenMode();
ZenLog.traceSetZenMode(zen, reason);
mZenMode = zen;
//根据勿扰模式的状态设置Settings数据库值
setZenModeSetting(mZenMode);
//更新勿扰模式统一的策略
updateConsolidatedPolicy(reason);
//更新铃声模式受影响的铃声流
updateRingerModeAffectedStreams();
if (setRingerMode && (zen != zenBefore || (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
&& policyHashBefore != mConsolidatedPolicy.hashCode()))) {
//更新铃声模式
applyZenToRingerMode();
}
//引用勿扰模式的各项限制
applyRestrictions();
if (zen != zenBefore) {
//回调方法通知勿扰模式发生改变
mHandler.postDispatchOnZenModeChanged();
}
}
private int computeZenMode() {
if (mConfig == null) return Global.ZEN_MODE_OFF;
synchronized (mConfig) {
if (mConfig.manualRule != null) return mConfig.manualRule.zenMode;
int zen = Global.ZEN_MODE_OFF;
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
if (zenSeverity(automaticRule.zenMode) > zenSeverity(zen)) {
// automatic rule triggered dnd and user hasn't seen update dnd dialog
if (Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, 1) == 0) {
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 1);
}
zen = automaticRule.zenMode;
}
}
}
return zen;
}
}
@VisibleForTesting
protected void setZenModeSetting(int zen) {
Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zen);
showZenUpgradeNotification(zen);
}
private void updateConsolidatedPolicy(String reason) {
if (mConfig == null) return;
synchronized (mConfig) {
ZenPolicy policy = new ZenPolicy();
if (mConfig.manualRule != null) {
applyCustomPolicy(policy, mConfig.manualRule);
}
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
applyCustomPolicy(policy, automaticRule);
}
}
Policy newPolicy = mConfig.toNotificationPolicy(policy);
if (!Objects.equals(mConsolidatedPolicy, newPolicy)) {
mConsolidatedPolicy = newPolicy;
dispatchOnConsolidatedPolicyChanged();
ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason);
}
}
}
@VisibleForTesting
protected void applyRestrictions() {
final boolean zenOn = mZenMode != Global.ZEN_MODE_OFF;
final boolean zenPriorityOnly = mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
final boolean zenSilence = mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
final boolean zenAlarmsOnly = mZenMode == Global.ZEN_MODE_ALARMS;
//bug 690574,liuningli.wt,modify,20210910,modify for allow the calls from contacts or starred contacts to play audio.
final boolean allowCalls = mConsolidatedPolicy.allowCalls()
&& (mConsolidatedPolicy.allowCallsFrom() == PRIORITY_SENDERS_ANY
|| mConsolidatedPolicy.allowCallsFrom() == PRIORITY_SENDERS_CONTACTS
|| mConsolidatedPolicy.allowCallsFrom() == PRIORITY_SENDERS_STARRED);
final boolean allowRepeatCallers = mConsolidatedPolicy.allowRepeatCallers();
final boolean allowSystem = mConsolidatedPolicy.allowSystem();
final boolean allowMedia = mConsolidatedPolicy.allowMedia();
final boolean allowAlarms = mConsolidatedPolicy.allowAlarms();
// notification restrictions
final boolean muteNotifications = zenOn
|| (mSuppressedEffects & SUPPRESSED_EFFECT_NOTIFICATIONS) != 0;
// call restrictions
final boolean muteCalls = zenAlarmsOnly
|| (zenPriorityOnly && !(allowCalls || allowRepeatCallers))
|| (mSuppressedEffects & SUPPRESSED_EFFECT_CALLS) != 0;
// alarm restrictions
final boolean muteAlarms = zenPriorityOnly && !allowAlarms;
// media restrictions
final boolean muteMedia = zenPriorityOnly && !allowMedia;
// system restrictions
final boolean muteSystem = zenAlarmsOnly || (zenPriorityOnly && !allowSystem);
// total silence restrictions
final boolean muteEverything = zenSilence || (zenPriorityOnly
&& ZenModeConfig.areAllZenBehaviorSoundsMuted(mConsolidatedPolicy));
for (int usage : AudioAttributes.SDK_USAGES) {
final int suppressionBehavior = AudioAttributes.SUPPRESSIBLE_USAGES.get(usage);
if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_NEVER) {
applyRestrictions(zenPriorityOnly, false /*mute*/, usage);
} else if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_NOTIFICATION) {
applyRestrictions(zenPriorityOnly, muteNotifications || muteEverything, usage);
} else if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_CALL) {
applyRestrictions(zenPriorityOnly, muteCalls || muteEverything, usage);
} else if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_ALARM) {
applyRestrictions(zenPriorityOnly, muteAlarms || muteEverything, usage);
} else if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_MEDIA) {
applyRestrictions(zenPriorityOnly, muteMedia || muteEverything, usage);
} else if (suppressionBehavior == AudioAttributes.SUPPRESSIBLE_SYSTEM) {
if (usage == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) {
// normally DND will only restrict touch sounds, not haptic feedback/vibrations
applyRestrictions(zenPriorityOnly, muteSystem || muteEverything, usage,
AppOpsManager.OP_PLAY_AUDIO);
applyRestrictions(zenPriorityOnly, false, usage, AppOpsManager.OP_VIBRATE);
} else {
applyRestrictions(zenPriorityOnly, muteSystem || muteEverything, usage);
}
} else {
applyRestrictions(zenPriorityOnly, muteEverything, usage);
}
}
}
最终通过AppOpsManager的setRestriction()方法将音频限制下发给底层。
*在流级别设置一个非持久的音频操作限制。
*限制是强加在持久规则之上的临时附加约束。
frameworks/base/core/java/android/app/AppOpsManager.java
/**
* Set a non-persisted restriction on an audio operation at a stream-level.
* Restrictions are temporary additional constraints imposed on top of the persisted rules
* defined by {@link #setMode}.
*
* @param code The operation to restrict.
* @param usage The {@link android.media.AudioAttributes} usage value.
* @param mode The restriction mode (MODE_IGNORED,MODE_ERRORED) or MODE_ALLOWED to unrestrict.
* @param exceptionPackages Optional list of packages to exclude from the restriction.
* @hide
*/
@RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES)
@UnsupportedAppUsage
public void setRestriction(int code, @AttributeUsage int usage, @Mode int mode,
String[] exceptionPackages) {
try {
final int uid = Binder.getCallingUid();
mService.setAudioRestriction(code, usage, uid, mode, exceptionPackages);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
3.2勿扰模式配置详解
上节我们主要讲解了勿扰模式的开关流程,上面提到了几个概念ZenModeConfig、automaticRule、manualRule、ZenPolicy,那么他们代表什么意思,互相之间又有什么关系,这节我们来探讨一下。
首先我们来看一下ZenModeHelper的加载流程:
- 创建ZenModeHelper对象,创建时会读取默认的勿扰模式配置
- 设置ZenModeHelper回调方法,在ZenMode状态和各项配置变化之后回调
- 从policyFile(/data/system/notification_policy.xml)中读取当前勿扰模式配置
- 初始化勿扰模式配置
- 设置勿扰模式白名单应用,仅在ZEN_MODE_IMPORTANT_INTERRUPTIONS模式下生效
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
@Override
public void onStart() {
init(..., new ConditionProviders(getContext(), mUserProfiles, AppGlobals.getPackageManager()),
new AtomicFile(new File(systemDir, "notification_policy.xml"), "notification-policy"), ...);
}
// TODO: All tests should use this init instead of the one-off setters above.
@VisibleForTesting
void init(..., ConditionProviders conditionProviders,..., AtomicFile policyFile, ...) {
......
mConditionProviders = conditionProviders;
//创建ZenModeHelper对象,创建时会读取默认的勿扰模式配置
mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders,
new SysUiStatsEvent.BuilderFactory());
//设置ZenModeHelper回调方法,在ZenMode状态和各项配置变化之后回调
mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
@Override
public void onConfigChanged() {
handleSavePolicyFile();
}
@Override
void onZenModeChanged() {
Binder.withCleanCallingIdentity(() -> {
sendRegisteredOnlyBroadcast(ACTION_INTERRUPTION_FILTER_CHANGED);
getContext().sendBroadcastAsUser(
new Intent(ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT),
UserHandle.ALL, permission.MANAGE_NOTIFICATIONS);
synchronized (mNotificationLock) {
updateInterruptionFilterLocked();
}
mRankingHandler.requestSort();
});
}
@Override
void onPolicyChanged() {
Binder.withCleanCallingIdentity(() -> {
sendRegisteredOnlyBroadcast(
NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED);
mRankingHandler.requestSort();
});
}
@Override
void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) {
Binder.withCleanCallingIdentity(() -> {
Intent intent = new Intent(ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED);
intent.setPackage(pkg);
intent.putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, id);
intent.putExtra(EXTRA_AUTOMATIC_ZEN_RULE_STATUS, status);
getContext().sendBroadcastAsUser(intent, UserHandle.of(userId));
});
}
});
......
//从policyFile(/data/system/notification_policy.xml)中读取当前勿扰模式配置
mPolicyFile = policyFile;
loadPolicyFile();
......
//初始化勿扰模式配置
mZenModeHelper.initZenMode();
....
//设置勿扰模式白名单应用,仅在ZEN_MODE_IMPORTANT_INTERRUPTIONS模式下生效
mZenModeHelper.setPriorityOnlyDndExemptPackages(getContext().getResources().getStringArray(
com.android.internal.R.array.config_priorityOnlyDndExemptPackages));
}
@VisibleForTesting
protected void loadPolicyFile() {
if (DBG) Slog.d(TAG, "loadPolicyFile");
synchronized (mPolicyFile) {
InputStream infile = null;
try {
infile = mPolicyFile.openRead();
readPolicyXml(infile, false /*forRestore*/, UserHandle.USER_ALL);
}
......
}
}
void readPolicyXml(InputStream stream, boolean forRestore, int userId)
throws XmlPullParserException, NumberFormatException, IOException {
......
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (ZenModeConfig.ZEN_TAG.equals(parser.getName())) {
mZenModeHelper.readXml(parser, forRestore, userId);
}
......
}
......
}
frameworks/base/core/res/res/values/config.xml
<!-- An array of packages that can make sound on the ringer stream in priority-only DND mode -->
<string-array translatable="false" name="config_priorityOnlyDndExemptPackages">
<item>com.android.dialer</item>
<item>com.android.systemui</item>
<item>android</item>
</string-array>
frameworks/base/services/core/java/com/android/server/notification/ZenModeHelper.java
public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders,
SysUiStatsEvent.BuilderFactory statsEventBuilderFactory) {
mContext = context;
mHandler = new H(looper);
addCallback(mMetrics);
mAppOps = context.getSystemService(AppOpsManager.class);
mNotificationManager = context.getSystemService(NotificationManager.class);
//读取默认的勿扰模式配置
mDefaultConfig = readDefaultConfig(mContext.getResources());
updateDefaultAutomaticRuleNames();
mConfig = mDefaultConfig.copy();
mConfigs.put(UserHandle.USER_SYSTEM, mConfig);
mConsolidatedPolicy = mConfig.toNotificationPolicy();
mSettingsObserver = new SettingsObserver(mHandler);
mSettingsObserver.observe();
mFiltering = new ZenModeFiltering(mContext);
mConditions = new ZenModeConditions(this, conditionProviders);
mServiceConfig = conditionProviders.getConfig();
mStatsEventBuilderFactory = statsEventBuilderFactory;
}
private ZenModeConfig readDefaultConfig(Resources resources) {
XmlResourceParser parser = null;
try {
parser = resources.getXml(R.xml.default_zen_mode_config);
while (parser.next() != XmlPullParser.END_DOCUMENT) {
final ZenModeConfig config = ZenModeConfig.readXml(parser);
if (config != null) return config;
}
} catch (Exception e) {
Log.w(TAG, "Error reading default zen mode config from resource", e);
} finally {
IoUtils.closeQuietly(parser);
}
return new ZenModeConfig();
}
public void readXml(XmlPullParser parser, boolean forRestore, int userId)
throws XmlPullParserException, IOException {
ZenModeConfig config = ZenModeConfig.readXml(parser);
String reason = "readXml";
if (config != null) {
if (forRestore) {
config.user = userId;
config.manualRule = null; // don't restore the manual rule
}
// booleans to determine whether to reset the rules to the default rules
boolean allRulesDisabled = true;
boolean hasDefaultRules = config.automaticRules.containsAll(
ZenModeConfig.DEFAULT_RULE_IDS);
long time = System.currentTimeMillis();
if (config.automaticRules != null && config.automaticRules.size() > 0) {
for (ZenRule automaticRule : config.automaticRules.values()) {
if (forRestore) {
// don't restore transient state from restored automatic rules
automaticRule.snoozing = false;
automaticRule.condition = null;
automaticRule.creationTime = time;
}
allRulesDisabled &= !automaticRule.enabled;
}
}
if (!hasDefaultRules && allRulesDisabled
&& (forRestore || config.version < ZenModeConfig.XML_VERSION)) {
// reset zen automatic rules to default on restore or upgrade if:
// - doesn't already have default rules and
// - all previous automatic rules were disabled
config.automaticRules = new ArrayMap<>();
for (ZenRule rule : mDefaultConfig.automaticRules.values()) {
config.automaticRules.put(rule.id, rule);
}
reason += ", reset to default rules";
}
// Resolve user id for settings.
userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
if (config.version < ZenModeConfig.XML_VERSION) {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId);
} else {
// devices not restoring/upgrading already have updated zen settings
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId);
}
if (DEBUG) Log.d(TAG, reason);
synchronized (mConfig) {
setConfigLocked(config, null, reason);
}
}
}
public void initZenMode() {
if (DEBUG) Log.d(TAG, "initZenMode");
evaluateZenMode("init", true /*setRingerMode*/);
}
默认的勿扰模式配置
frameworks/base/core/res/res/xml/default_zen_mode_config.xml
<!-- Default configuration for zen mode. See android.service.notification.ZenModeConfig. -->
<zen version="9">
<allow alarms="true" media="true" system="false" calls="true" callsFrom="2" messages="false"
reminders="false" events="false" repeatCallers="true" convos="false"
convosFrom="3"/>
<automatic ruleId="EVENTS_DEFAULT_RULE" enabled="false" snoozing="false" name="Event" zen="1"
component="android/com.android.server.notification.EventConditionProvider"
conditionId="condition://android/event?userId=-10000&calendar=&reply=1"/>
<automatic ruleId="EVERY_NIGHT_DEFAULT_RULE" enabled="false" snoozing="false" name="Sleeping"
zen="1" component="android/com.android.server.notification.ScheduleConditionProvider"
conditionId="condition://android/schedule?days=1.2.3.4.5.6.7&start=22.0&end=7.0&exitAtAlarm=true"/>
<!-- all visual effects that exist as of P -->
<disallow visualEffects="511" />
<!-- whether there are notification channels that can bypass dnd -->
<state areChannelsBypassingDnd="false" />
</zen>
打开勿扰模式之后的勿扰模式配置
/data/system/notification_policy.xml
<zen version="8" user="0">
<allow calls="false" repeatCallers="false" messages="false" reminders="false" events="false"
callsFrom="0" messagesFrom="1" alarms="true" media="true" system="false" convos="false" convosFrom="3" />
<disallow visualEffects="511" />
<manual enabled="true" zen="1" creationTime="0" modified="false" />
<automatic ruleId="EVENTS_DEFAULT_RULE"
enabled="false"
name="活动"
zen="1"
component="android/com.android.server.notification.EventConditionProvider"
conditionId="condition://android/event?userId=-10000&calendar=&reply=1"
creationTime="0"
id="condition://android/event?userId=-10000&calendar=&reply=1"
summary="..." line1="..." line2="..." icon="0" state="0" flags="2" modified="false" />
<automatic ruleId="EVERY_NIGHT_DEFAULT_RULE"
enabled="false"
name="睡眠"
zen="1"
component="android/com.android.server.notification.ScheduleConditionProvider"
conditionId="condition://android/schedule?
days=1.2.3.4.5.6.7&start=22.0&end=7.0&exitAtAlarm=true"
creationTime="0"
id="condition://android/schedule? days=1.2.3.4.5.6.7&start=22.0&end=7.0&exitAtAlarm=true"
summary="..." line1="..." line2="..." icon="0" state="0" flags="2"
callsFrom="3" messagesFrom="4" repeatCallers="1" alarms="1" media="2" system="2" reminders="2" vents="2"
showFullScreenIntent="2" showLights="2" shoePeek="2" showStatusBarIcons="2" showBadges="2" showAmbient="2"
showNotificationList="2" modified="false" />
<state areChannelsBypassingDnd="false" />
</zen>
通过以上的配置信息,可以很清楚地了解勿扰模式的配置数据结构,ZenModeConfig是一个总的数据结构,包括<zen>里面的所有信息
- 其中allow标签表示"不受勿扰模式限制的例外项"中的配置项:
calls="false" 通话
repeatCallers="false" 不屏蔽重复来电者
callsFrom="0" 例外的通话:星标联系人,联系人,任何人,无
messages="false" 消息
messagesFrom="1" 例外的消息:星标联系人,联系人,任何人,无
convos="false" 对话
convosFrom="3"例外的对话:所有对话,优先对话,无
reminders="false" 提醒
events="false" 日历活动
alarms="true" 闹钟
media="true" 媒体声音
system="false" 触摸提示音 - disallow标签 visualEffects表示视觉效果,对应“隐藏通知的显示方式”菜单项,结果以二进制存储
- manualRule对应manual标签,表示用户手动打开勿扰模式。
-
automaticRule对应automatic标签,表示系统自动开启的勿扰模式规则,对应“时间表菜单”,这里是用户预设的定时勿扰模式,在预设时间会自动打开或者关闭勿扰模式。
- areChannelsBypassingDnd表示是否开启例外的应用
至于ZenPolicy,并没有在xml中存储,而是在代码中动态生成的,上面我们所讲的Rule是勿扰的规则,但是这些规则可能并没有启用或者只启用了一部分,我们可以通过enabled查看它是否启用。而ZenPolicy代表勿扰模式的策略,它是综合了所有规则形成了一个统一的并且实时的策略mConsolidatedPolicy,通过策略可以获知当前已经生效的勿扰规则,从而对系统各项功能进行设置。
如下是mConsolidatedPolicy初始化和更新代码:
public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders,
......
mConsolidatedPolicy = mConfig.toNotificationPolicy();
......
}
private void updateConsolidatedPolicy(String reason) {
if (mConfig == null) return;
synchronized (mConfig) {
ZenPolicy policy = new ZenPolicy();
if (mConfig.manualRule != null) {
applyCustomPolicy(policy, mConfig.manualRule);
}
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
applyCustomPolicy(policy, automaticRule);
}
}
Policy newPolicy = mConfig.toNotificationPolicy(policy);
if (!Objects.equals(mConsolidatedPolicy, newPolicy)) {
mConsolidatedPolicy = newPolicy;
dispatchOnConsolidatedPolicyChanged();
ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason);
}
}
}
以上,我们对勿扰模式就分析完成了。
4.默认铃声设置详解
默认铃声设置主要有手机铃声、默认通知提示音、默认闹钟提示音三个选项,定义如下:
<!-- 手机铃声 -->
<com.android.settings.DefaultRingtonePreference
android:key="phone_ringtone"
android:title="@string/ringtone_title"
android:dialogTitle="@string/ringtone_title"
android:summary="@string/summary_placeholder"
android:ringtoneType="ringtone"
android:order="-100"
settings:keywords="@string/sound_settings"/>
<!-- 默认通知提示音 -->
<com.android.settings.DefaultRingtonePreference
android:key="notification_ringtone"
android:title="@string/notification_ringtone_title"
android:dialogTitle="@string/notification_ringtone_title"
android:summary="@string/summary_placeholder"
android:ringtoneType="notification"
android:order="-90"/>
<!-- 默认闹钟提示音 -->
<com.android.settings.DefaultRingtonePreference
android:key="alarm_ringtone"
android:title="@string/alarm_ringtone_title"
android:dialogTitle="@string/alarm_ringtone_title"
android:summary="@string/summary_placeholder"
android:persistent="false"
android:ringtoneType="alarm"
android:order="-80"/>
这三个选项基本逻辑相同,主要通过android:ringtoneType区分,我们就以手机铃声为例讲解铃声的设置流程。
4.1 Settings的逻辑
SoundSettings是声音界面的fragment,它会监听preference的click事件,在click事件中判断如果是RingtonePreference,就调用RingtonePreference的onPrepareRingtonePickerIntent方法准备铃声界面的intent,准备完成之后就去驱动目标intent。这里用startActivityForResultAsUser方法,是因为我们需要获取RingtonePicker返回来的数据。
vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/notification/SoundSettings.java
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference instanceof RingtonePreference) {
writePreferenceClickMetric(preference);
mRequestPreference = (RingtonePreference) preference;
mRequestPreference.onPrepareRingtonePickerIntent(mRequestPreference.getIntent());
getActivity().startActivityForResultAsUser(
mRequestPreference.getIntent(),
REQUEST_CODE,
null,
UserHandle.of(mRequestPreference.getUserId()));
return true;
}
return super.onPreferenceTreeClick(preference);
}
我们看到,RingtonePreference初始化时给intent指定了action:ACTION_RINGTONE_PICKER,然后onPrepareRingtonePickerIntent()方法中给ringtonePickerIntent添加了很多参数,ACTION_RINGTONE_PICKER匹配到com.android.soundpicker/.RingtonePickerActivity,打开该界面。
- EXTRA_RINGTONE_EXISTING_URI:当前选中的铃声uri
- EXTRA_RINGTONE_SHOW_DEFAULT:是否显示默认的铃声,默认为true,但在DefaultRingtonePreference设置为false了
- EXTRA_RINGTONE_DEFAULT_URI:默认铃声uri
- EXTRA_RINGTONE_SHOW_SILENT:是否显示"无声"
- EXTRA_RINGTONE_TYPE:铃声类型(手机铃声、通知铃声、闹钟铃声)
- EXTRA_RINGTONE_TITLE:标题
- EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS:audio flag,这里传入FLAG_BYPASS_INTERRUPTION_POLICY,表示即使在有声音限制的情况下也播放声音
选择完成之后退出RingtonePickerActivity,会回调onActivityResult()方法并携带ringtone uri,然后调用onSaveRingtone()方法进一步处理uri。
vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/RingtonePreference.java
public class RingtonePreference extends Preference {
private int mRingtoneType;
private boolean mShowDefault;
private boolean mShowSilent;
private int mRequestCode;
protected int mUserId;
protected Context mUserContext;
public RingtonePreference(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.RingtonePreference, 0, 0);
mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType,
RingtoneManager.TYPE_RINGTONE);
mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault,
true);
mShowSilent = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showSilent,
true);
setIntent(new Intent(RingtoneManager.ACTION_RINGTONE_PICKER));
setUserId(UserHandle.myUserId());
a.recycle();
}
......
/**
* Prepares the intent to launch the ringtone picker. This can be modified
* to adjust the parameters of the ringtone picker.
*
* @param ringtonePickerIntent The ringtone picker intent that can be
* modified by putting extras.
*/
public void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
onRestoreRingtone());
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, mShowDefault);
if (mShowDefault) {
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
RingtoneManager.getDefaultUri(getRingtoneType()));
}
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, mShowSilent);
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, mRingtoneType);
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getTitle());
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY);
}
/**
* Called when a ringtone is chosen.
* <p>
* By default, this saves the ringtone URI to the persistent storage as a
* string.
*
* @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null.
*/
protected void onSaveRingtone(Uri ringtoneUri) {
persistString(ringtoneUri != null ? ringtoneUri.toString() : "");
}
/**
* Called when the chooser is about to be shown and the current ringtone
* should be marked. Can return null to not mark any ringtone.
* <p>
* By default, this restores the previous ringtone URI from the persistent
* storage.
*
* @return The ringtone to be marked as the current ringtone.
*/
protected Uri onRestoreRingtone() {
final String uriString = getPersistedString(null);
return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null;
}
......
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (callChangeListener(uri != null ? uri.toString() : "")) {
onSaveRingtone(uri);
}
}
return true;
}
}
onSaveRingtone()方法在子类DefaultRingtonePreference 进行重写,调用RingtoneManager setActualDefaultRingtoneUri()方法设置铃声。
vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/DefaultRingtonePreference.java
public class DefaultRingtonePreference extends RingtonePreference {
private static final String TAG = "DefaultRingtonePreference";
public DefaultRingtonePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
super.onPrepareRingtonePickerIntent(ringtonePickerIntent);
/*
* Since this preference is for choosing the default ringtone, it
* doesn't make sense to show a 'Default' item.
*/
ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
}
@Override
protected void onSaveRingtone(Uri ringtoneUri) {
RingtoneManager.setActualDefaultRingtoneUri(mUserContext, getRingtoneType(), ringtoneUri);
}
@Override
protected Uri onRestoreRingtone() {
return RingtoneManager.getActualDefaultRingtoneUri(mUserContext, getRingtoneType());
}
}
4.2 soundpicker中的逻辑
上节说到了startActivityForResultAsUser()之后会调起RingtonePickerActivity,我们继续来看下RingtonePickerActivity是怎样的加载数据的。
RingtonePickerActivity继承AlertActivity,是一个弹窗界面,所有的参数最终都会设置到AlertParams中。onCreate中,首先会初始化ringtone数据,然后构造adapter适配器,最后设置AlertParams。
frameworks/base/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1);
......
//初始化ringtone数据
initRingtoneManager();
......
// Get whether to show the 'Silent' item
mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
// AudioAttributes flags
mAttributesFlags |= intent.getIntExtra(
RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
0 /*defaultValue == no flags*/);
mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
// The volume keys will control the stream that we are choosing a ringtone for
setVolumeControlStream(mRingtoneManager.inferStreamType());
// Get the URI whose list item should have a checkmark
mExistingUri = intent
.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
// Create the list of ringtones and hold on to it so we can update later.
mAdapter = new BadgedRingtoneAdapter(this, mCursor,
/* isManagedProfile = */ UserManager.get(this).isManagedProfile(mPickerUserId));
......
//设置alert的参数
final AlertController.AlertParams p = mAlertParams;
p.mAdapter = mAdapter;
p.mOnClickListener = mRingtoneClickListener;
p.mLabelColumn = COLUMN_LABEL;
p.mIsSingleChoice = true;
p.mOnItemSelectedListener = this;
if (mShowOkCancelButtons) {
p.mPositiveButtonText = getString(com.android.internal.R.string.ok);
p.mPositiveButtonListener = this;
p.mNegativeButtonText = getString(com.android.internal.R.string.cancel);
p.mPositiveButtonListener = this;
}
p.mOnPrepareListViewListener = this;
p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
......
setupAlert();
}
初始化ringtone数据时,从RingtoneManager获取Cursor,构造一个本地Cursor
private void initRingtoneManager() {
// Reinstantiate the RingtoneManager. Cursor.requery() was deprecated and calling it
// causes unexpected behavior.
mRingtoneManager = new RingtoneManager(mTargetContext, /* includeParentRingtones */ true);
if (mType != -1) {
mRingtoneManager.setType(mType);
}
mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL);
}
RingtoneManager getCursor()通过getInternalRingtones()、getMediaRingtones()和getParentProfileRingtones()三个方法查询铃声,最终通过mediaprovider查询到所有可用铃声。
frameworks/base/media/java/android/media/RingtoneManager.java
/**
* Returns a {@link Cursor} of all the ringtones available. The returned
* cursor will be the same cursor returned each time this method is called,
* so do not {@link Cursor#close()} the cursor. The cursor can be
* {@link Cursor#deactivate()} safely.
* <p>
* If {@link RingtoneManager#RingtoneManager(Activity)} was not used, the
* caller should manage the returned cursor through its activity's life
* cycle to prevent leaking the cursor.
* <p>
* Note that the list of ringtones available will differ depending on whether the caller
* has the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission.
*
* @return A {@link Cursor} of all the ringtones available.
* @see #ID_COLUMN_INDEX
* @see #TITLE_COLUMN_INDEX
* @see #URI_COLUMN_INDEX
*/
public Cursor getCursor() {
if (mCursor != null && mCursor.requery()) {
return mCursor;
}
ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
ringtoneCursors.add(getInternalRingtones());
ringtoneCursors.add(getMediaRingtones());
if (mIncludeParentRingtones) {
Cursor parentRingtonesCursor = getParentProfileRingtones();
if (parentRingtonesCursor != null) {
ringtoneCursors.add(parentRingtonesCursor);
}
}
return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
}
@UnsupportedAppUsage
private Cursor getInternalRingtones() {
final Cursor res = query(
MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
constructBooleanTrueWhereClause(mFilterColumns),
null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
}
private Cursor getMediaRingtones() {
final Cursor res = getMediaRingtones(mContext);
return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
}
mAlert 有一个重要的参数p.mOnClickListener = mRingtoneClickListener,监听ringtonePicker的点击事件。
点击时间有两种情况:
- 点击到铃声item,这时走check item逻辑,选中所点击的item之后,播放对应的铃声
- 点击到“添加铃声”,这时会启动chooseFile界面,跳转到文件管理器中选择音频文件,返回之后回调onActivityResult()方法。
private DialogInterface.OnClickListener mRingtoneClickListener =
new DialogInterface.OnClickListener() {
/*
* On item clicked
*/
public void onClick(DialogInterface dialog, int which) {
if (which == mCursor.getCount() + mStaticItemCount) {
// The "Add new ringtone" item was clicked. Start a file picker intent to select
// only audio files (MIME type "audio/*")
final Intent chooseFile = getMediaFilePickerIntent();
startActivityForResult(chooseFile, ADD_FILE_REQUEST_CODE);
return;
}
// Save the position of most recently clicked item
setCheckedItem(which);
// In the buttonless (watch-only) version, preemptively set our result since we won't
// have another chance to do so before the activity closes.
if (!mShowOkCancelButtons) {
setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
}
// Play clip
playRingtone(which, 0);
}
};
onActivityResult()方法中在异步线程中调用addCustomExternalRingtone()方法将铃声添加到RingtoneManager中,结束之后调用requeryForAdapter()重新查询铃声数据并刷新界面。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "onActivityResult: requestCode = "+requestCode+", resultCode = "+resultCode);
if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
// Add the custom ringtone in a separate thread
final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() {
@Override
protected Uri doInBackground(Uri... params) {
try {
return mRingtoneManager.addCustomExternalRingtone(params[0], mType);
} catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Unable to add new ringtone", e);
}
return null;
}
@Override
protected void onPostExecute(Uri ringtoneUri) {
if (ringtoneUri != null) {
requeryForAdapter();
} else {
// Ringtone was not added, display error Toast
Toast.makeText(RingtonePickerActivity.this, R.string.unable_to_add_ringtone,
Toast.LENGTH_SHORT).show();
}
}
};
installTask.execute(data.getData());
}
}
/**
* Re-query RingtoneManager for the most recent set of installed ringtones. May move the
* selected item position to match the new position of the chosen sound.
*
* This should only need to happen after adding or removing a ringtone.
*/
private void requeryForAdapter() {
Log.d(TAG, "requeryForAdapter: ");
// Refresh and set a new cursor, closing the old one.
initRingtoneManager();
mAdapter.changeCursor(mCursor);
// Update checked item location.
int checkedPosition = POS_UNKNOWN;
for (int i = 0; i < mAdapter.getCount(); i++) {
if (mAdapter.getItemId(i) == mCheckedItemId) {
checkedPosition = getListPosition(i);
break;
}
}
if (mHasSilentItem && checkedPosition == POS_UNKNOWN) {
checkedPosition = mSilentPos;
}
setCheckedItem(checkedPosition);
setupAlert();
}