针对Audio Jack的设备物理组件的存在性判断,及耳机是否在位的判断。
官方文档中提及的词:
Android 6 (API 23)及以上设备测试:
判断是否支持音频输出。
final PackageManager pm = context.getPackageManager();
boolean hasAudioOutput = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT);
再判定输出设备类型。
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
测试设备可以获取到两个值:
两台设备,一台有耳机孔无USB口,另一台设备有USB插口但无耳机插孔。
即 耳机插入 符合 AudioDeviceInfo.TYPE_BUILTIN_EARPIECE 类型设备, 扬声器 符合 AudioDeviceInfo.TYPE_BUILTIN_SPEAKER 类型设备。
这个设备类型很容易理解,即内置扬声器,基本设备出厂时均会集成在设备内。
earpiece 指耳机类型,耳机孔设备属于此范围,而 USB类型的耳机 是否属于此范围,由于无设备测试,暂且无法明确。
但从程序运行角度看,无耳机插孔的设备,也同样返回了 TYPE_BUILTIN_EARPIECE 的输出类型。以此推测,USB类型耳机也属于这个范畴。
根据判断,需要检查当前音频输出是Audio Jack还是speaker扬声器。
判断headphone是否在位(插入耳机孔判断)
final Intent intent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
final boolean headsetExists = intent.getIntExtra("state", 0) == 1;
在匹配的Intent.ACTION_HEADSET_PLUG广播Intent中包含有参数state,表明headphone设备是否插入耳机孔。
MediaRouter 判断
final MediaRouter mr = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
final MediaRouter.RouteInfo ri = mr.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
final CharSequence routeName = ri.getName(); // Headphones
判断媒体路由,获取当前音频路由输出。getName() 方法返回用户可见的字符串方式,返回系统字符串资源的定义。
<resources>
<string name="default_audio_route_name" product="tablet">Tabletstring>
<string name="default_audio_route_name" product="tv">TVstring>
<string name="default_audio_route_name" product="default">Phonestring>
<string name="default_audio_route_name_dock_speakers">Dock speakersstring>
<string name="default_audio_route_name_hdmi">HDMIstring>
<string name="default_audio_route_name_headphones">Headphonesstring>
<string name="default_audio_route_name_usb">USBstring>
resources>
若不插入耳机的情况,需要看OS是何种产品类型:
平板(tablet) => Tablet
手机(default) => Phone
TV(tv) => TV
其他的音频路由输出也有对应的系统值定义。
在Android平台版本迭代过程中,新API不断出现,旧API会被标记Deprecated。虽然deprecated的API依然可以使用,但在不断迭代过程中,其起到的作用慢慢不太符合对应的需求,或者其原有实现被分解更加详细地实现。这里我遇到的耳机的状态判断就是其中一种情况。
在API 15(含)后被标记为deprecated的isWiredHeadsetOn()这个方法,一般就被使用来判断是否有耳机设备连接到当前Android设备。
/**
* Checks whether a wired headset is connected or not.
* This is not a valid indication that audio playback is
* actually over the wired headset as audio routing depends on other conditions.
*
* @return true if a wired headset is connected.
* false if otherwise
* @deprecated Use {@link AudioManager#getDevices(int)} instead to list available audio devices.
*/
public boolean isWiredHeadsetOn() {
if (AudioSystem.getDeviceConnectionState(DEVICE_OUT_WIRED_HEADSET,"")
== AudioSystem.DEVICE_STATE_UNAVAILABLE &&
AudioSystem.getDeviceConnectionState(DEVICE_OUT_WIRED_HEADPHONE,"")
== AudioSystem.DEVICE_STATE_UNAVAILABLE &&
AudioSystem.getDeviceConnectionState(DEVICE_OUT_USB_HEADSET, "")
== AudioSystem.DEVICE_STATE_UNAVAILABLE) {
return false;
} else {
return true;
}
}
其源码实现中判断了WIRED_HEADSET,WIRED_HEADPHONE,USB_HEADSET三种有线设备的状态。一般情况下均可以使用此方法进行是否连接了外部的音频设备,因为Android设备外设接口类型可用作支持的音频的只有耳机孔及USB。但在一种场景下,此方法无法具体判断是何种外接设备在位——是耳机在耳机孔内亦或USB设备连接上。
此值定义是广播动作(action),用以接收有线的插拔状态广播。在文档说明中,此action起作用只能使用Context#registerReceiver(BroadcastReceiver, IntentFilter)方式,在manifest进行注册的方式将无法接收到此广播消息。
另外在Intent中包含有其他消息数据:
这些值均比较容易理解,由于测试设备并非手机,并无phone模块,因此不知是否会影响到name,microphone的值。在我的测试场景中name值null,microphone也未准确获取到(由于OS是定制的,可能是下边修改的,如果知道确切原因的请在评论中告知我)。
一般Broadcast的action动作接收均是通过定义BroadcastReceiver后接收ACTION_HEADSET_PLUG的广播,后在回调方法内进行相关处理。若仔细看Context#registerReceiver(BroadcastReceiver, IntentFilter)的签名,可以看到起返回是一个Intent对象。AudioManager.ACTION_HEADSET_PLUG广播的定义有对应的数据说明。
/**
* Broadcast Action: Wired Headset plugged in or unplugged.
*
* You cannot receive this through components declared
* in manifests, only by explicitly registering for it with
* {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)
* Context.registerReceiver()}.
*
* The intent will have the following extra values:
*
* - state - 0 for unplugged, 1 for plugged.
* - name - Headset type, human readable string
* - microphone - 1 if headset has a microphone, 0 otherwise
*
*
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_HEADSET_PLUG =
"android.intent.action.HEADSET_PLUG";
可以看到源码文档注释中的intent中携带的数据key,及类型。因此可以使用如下的代码获取当前的audio的headset状态。
final Intent intent = context
.registerReceiver(null, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
boolean headsetExists = false;
try {
Objects.requireNonNull(intent, "headset device does not exist!");
headsetExists = intent.getIntExtra("state", 0) == 1;
final String audioType = intent.getStringExtra("name");
final boolean hasMicrophone = intent.getIntExtra("", 0) == 1;
Log.d(TAG, String.format("headset exists? %b, audio type: %s, microphone exists? %b",
headsetExists, audioType, hasMicrophone));
} catch (NullPointerException e) { // headset does not exist
Log.i(TAG, "headset unplugged.");
headsetExists = false;
}
在调用registerReceiver(BroadcastReceiver, IntentFilter)时第一个参数出入null,这样告诉Android不接收ACTION_HEADSET_PLUG的动作,但需要匹配动作的sticky Intent返回。这里有个requireNonNull()方法的调用对返回的Intent进行判null操作,原因在于首次系统启动,且headset不在位的情况下,获取到的intent为null。因此我们多了一种获取headset是否在位的方式,且可以不使用deprecated的API,看着代码可读性会好很多。
PS: 这里的headset不仅是耳机,还包含了支持USB的音频输出设备。大多数情况下是可以满足使用的。