Android-设置-声音(音量调节 勿扰模式 铃声设置) 流程梳理

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的菜单界面和具体内容:


Screenshot_20210909-172048.png
<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小时(可自行设置)”、“每次都询问”三个选项。


持续时间.jpg

这里不同的分支最终都指向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的加载流程:

  1. 创建ZenModeHelper对象,创建时会读取默认的勿扰模式配置
  2. 设置ZenModeHelper回调方法,在ZenMode状态和各项配置变化之后回调
  3. 从policyFile(/data/system/notification_policy.xml)中读取当前勿扰模式配置
  4. 初始化勿扰模式配置
  5. 设置勿扰模式白名单应用,仅在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&amp;calendar=&amp;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&amp;start=22.0&amp;end=7.0&amp;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&amp;calendar=&amp;reply=1" 
               creationTime="0" 
               id="condition://android/event?userId=-10000&amp;calendar=&amp;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&amp;start=22.0&amp;end=7.0&amp;exitAtAlarm=true" 
               creationTime="0" 
               id="condition://android/schedule?    days=1.2.3.4.5.6.7&amp;start=22.0&amp;end=7.0&amp;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>里面的所有信息

  1. 其中allow标签表示"不受勿扰模式限制的例外项"中的配置项:
    不受勿扰模式限制的例外项.jpg

    calls="false" 通话
    repeatCallers="false" 不屏蔽重复来电者
    callsFrom="0" 例外的通话:星标联系人,联系人,任何人,无
    messages="false" 消息
    messagesFrom="1" 例外的消息:星标联系人,联系人,任何人,无
    convos="false" 对话
    convosFrom="3"例外的对话:所有对话,优先对话,无
    reminders="false" 提醒
    events="false" 日历活动
    alarms="true" 闹钟
    media="true" 媒体声音
    system="false" 触摸提示音
  2. disallow标签 visualEffects表示视觉效果,对应“隐藏通知的显示方式”菜单项,结果以二进制存储
  3. manualRule对应manual标签,表示用户手动打开勿扰模式。
  4. automaticRule对应automatic标签,表示系统自动开启的勿扰模式规则,对应“时间表菜单”,这里是用户预设的定时勿扰模式,在预设时间会自动打开或者关闭勿扰模式。


    image.png
  5. 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的点击事件。
点击时间有两种情况:

  1. 点击到铃声item,这时走check item逻辑,选中所点击的item之后,播放对应的铃声
  2. 点击到“添加铃声”,这时会启动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();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352