音量调节流程分析
- 按下音量键
音量键被按下后,按键事件会一路派发给Acitivity,如果无人拦截并处理,承载当前Activity的显示PhoneWindow类的onKeyDown()以及onKeyUp()函数将会被处理,从而开始通过音量键调整音量的处理流程;
按照输入事件的派发策略,Window对象在事件的派发队列中位于Acitivity的后面,所以应用程序可以重写自己的Activity.onKeyDown()函数以截获音量键的消息,并将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量;
Java层
PhoneWindow.Java
protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {
final KeyEvent.DispatcherState dispatcher =
mDecor != null ? mDecor.getKeyDispatcherState() : null;
//Log.i(TAG, "Key down: repeat=" + event.getRepeatCount()
// + " flags=0x" + Integer.toHexString(event.getFlags()));
switch (keyCode) {
//我们当前只要注重这三个音量键动作即可
case KeyEvent.KEYCODE_VOLUME_UP://音量上键
case KeyEvent.KEYCODE_VOLUME_DOWN://音量下键
case KeyEvent.KEYCODE_VOLUME_MUTE: {//静音
// If we have a session send it the volume command, otherwise
// use the suggested stream.
if (mMediaController != null) {
getMediaSessionManager().dispatchVolumeKeyEventToSessionAsSystemService(event,
mMediaController.getSessionToken());
} else {
getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(event,
mVolumeControlStreamType);
}
return true;
}
...
return false;
}
可以看到有两条条件逻辑进行接下来的音量调整,我们分别来看
MediaSessionManager.java
/**
* Dispatches the volume key event as system service to the session.
* <p>
* Should be only called by the {@link com.android.internal.policy.PhoneWindow} when the
* foreground activity didn't consume the key from the hardware devices.
*
* @param keyEvent the volume key event to send 要发送的音量键事件。
* @param sessionToken the session token to which the key event should be dispatched 指定的会话令牌,用于识别目标媒体会话。
* @hide
*/
//这个方法负责将音量键事件发送到指定的媒体会话,通常在前台活动未处理该事件时调用。
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public void dispatchVolumeKeyEventToSessionAsSystemService(@NonNull KeyEvent keyEvent,
@NonNull MediaSession.Token sessionToken) {
Objects.requireNonNull(sessionToken, "sessionToken shouldn't be null");
Objects.requireNonNull(keyEvent, "keyEvent shouldn't be null");
try {
//通过 mService 调用系统服务的相关方法,将音量键事件和会话令牌传递过去。
//mContext.getPackageName() 和 mContext.getOpPackageName() 提供了必要的上下文信息。
mService.dispatchVolumeKeyEventToSessionAsSystemService(mContext.getPackageName(),
mContext.getOpPackageName(), keyEvent, sessionToken);
} catch (RemoteException e) {
Log.wtf(TAG, "Error calling dispatchVolumeKeyEventAsSystemService", e);
}
}
/**
* Dispatches the volume button event as system service to the session. This only effects the
* {@link MediaSession.Callback#getCurrentControllerInfo()} and doesn't bypass any permission
* check done by the system service.
* <p>
* Should be only called by the {@link com.android.internal.policy.PhoneWindow} or
* {@link android.view.FallbackEventHandler} when the foreground activity didn't consume the key
* from the hardware devices.
* <p>
* Valid stream types include {@link AudioManager.PublicStreamTypes} and
* {@link AudioManager#USE_DEFAULT_STREAM_TYPE}.
*
* @param keyEvent the volume key event to send 要发送的音量键事件。
* @param streamType type of stream 指定的音频流类型,可以是公开流类型或默认流类型。
* @hide
*/
//这个方法负责将音量键事件发送到与指定流类型相关的媒体会话。
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public void dispatchVolumeKeyEventAsSystemService(@NonNull KeyEvent keyEvent, int streamType) {
//这个方法实际上调用了一个内部方法 dispatchVolumeKeyEventInternal,并传递了音量事件、流类型以及两个布尔参数,指示是否仅限于音乐流和是否作为系统服务处理。
dispatchVolumeKeyEventInternal(keyEvent, streamType, /*musicOnly=*/false,
/*asSystemService=*/true);
}
这两个方法调用的场景有较大的区别:
dispatchVolumeKeyEventToSessionAsSystemService
:
- 这个方法将音量键事件派发到特定的媒体会话,使用会话令牌。它主要用于处理与当前媒体会话相关的音量事件,适用于有活动的媒体控制。
dispatchVolumeKeyEventAsSystemService
:
- 这个方法将音量键事件派发到与指定流类型相关的媒体会话。它不依赖于具体的媒体会话,而是根据流类型进行处理,适用于更广泛的音量事件派发场景。
简单举个例子就是:
假设你正在开发一个视频播放器应用,用户在观看视频时可以使用音量键来调整音量。我们将在不同情况下处理音量键事件。
- 使用
dispatchVolumeKeyEventToSessionAsSystemService
场景:用户正在播放视频并按下音量键。我们希望将音量事件发送到正在播放的媒体会话。
// 获取当前的媒体控制器和音量键事件
MediaController mediaController = getMediaController(); // 当前正在播放的视频的 MediaController
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP);
if (mediaController != null) {
// 获取会话令牌
MediaSession.Token sessionToken = mediaController.getSessionToken();
// 将音量键事件派发到当前的媒体会话
getMediaSessionManager().dispatchVolumeKeyEventToSessionAsSystemService(keyEvent, sessionToken);
} else {
// 如果没有可用的媒体控制器,可以显示提示
Log.w("VideoPlayer", "No active media controller to handle volume event.");
}
- 使用
dispatchVolumeKeyEventAsSystemService
场景:用户在应用的设置页面按下音量键,但当前没有视频播放或媒体会话在活动。我们想根据流类型调整音量。
// 获取音量键事件
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN);
int streamType = AudioManager.STREAM_MUSIC; // 选择音乐流类型
// 将音量键事件派发到系统服务
getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(keyEvent, streamType);
我们在这篇文档中着重理解一下第二种情况,不依赖于具体的媒体会话,而是根据流类型进行处理,适用于更广泛的音量事件派发场景。
private void dispatchVolumeKeyEventInternal(@NonNull KeyEvent keyEvent, int stream,
boolean musicOnly, boolean asSystemService) {
Objects.requireNonNull(keyEvent, "keyEvent shouldn't be null");
try {
mService.dispatchVolumeKeyEvent(mContext.getPackageName(), mContext.getOpPackageName(),
asSystemService, keyEvent, stream, musicOnly);
} catch (RemoteException e) {
Log.e(TAG, "Failed to send volume key event.", e);
}
}
我们需要知道这个mService是指哪个类
private final ISessionManager mService;
ISessionManager是一个aidl文件,属于跨进程通信的一种机制,这种文件看上去就跟Java中的接口有一样,在编译后会生成Java文件。
mService = ISessionManager.Stub.asInterface(MediaFrameworkPlatformInitializer
.getMediaServiceManager()
.getMediaSessionServiceRegisterer()
.get());
通过推测mService应该是MediaSessionService
MediaSessionService.java
/**
* Dispatches volume key events. This is called when the foreground activity didn't handle
* the incoming volume key event.
* <p>
* Handles the dispatching of the volume button events to one of the
* registered listeners. If there's a volume key long-press listener and
* there's no active global priority session, long-presses will be sent to the
* long-press listener instead of adjusting volume.
*
* @param packageName The caller's package name, obtained by Context#getPackageName()
* @param opPackageName The caller's op package name, obtained by Context#getOpPackageName()
* @param asSystemService {@code true} if the event sent to the session as if it was come
* from the system service instead of the app process. This helps sessions to
* distinguish between the key injection by the app and key events from the
* hardware devices. Should be used only when the volume key events aren't handled
* by foreground activity. {@code false} otherwise to tell session about the real
* caller.
* @param keyEvent a non-null KeyEvent whose key code is one of the
* {@link KeyEvent#KEYCODE_VOLUME_UP},
* {@link KeyEvent#KEYCODE_VOLUME_DOWN},
* or {@link KeyEvent#KEYCODE_VOLUME_MUTE}.
* @param stream stream type to adjust volume.
* @param musicOnly true if both UI and haptic feedback aren't needed when adjusting volume.
* @see #dispatchVolumeKeyEventToSessionAsSystemService
*/
@Override
public void dispatchVolumeKeyEvent(String packageName, String opPackageName,
boolean asSystemService, KeyEvent keyEvent, int stream, boolean musicOnly) {
if (keyEvent == null
|| (keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP
&& keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN
&& keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_MUTE)) {
Log.w(TAG, "Attempted to dispatch null or non-volume key event.");
return;
}
//获取调用者的进程 ID(PID)和用户 ID(UID),并清除调用身份,以确保后续的权限检查正确。
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
if (DEBUG_KEY_EVENT) {
Log.d(TAG, "dispatchVolumeKeyEvent, pkg=" + packageName
+ ", opPkg=" + opPackageName + ", pid=" + pid + ", uid=" + uid
+ ", asSystem=" + asSystemService + ", event=" + keyEvent
+ ", stream=" + stream + ", musicOnly=" + musicOnly);
}
try {
//重点在这
synchronized (mLock) {
//检查是否有全局优先级会话活动。如果有,则调用相应的方法派发音量事件;如果没有,则使用另一个处理器处理音量事件。
//全局优先例如语音助手、通话、紧急通知
if (isGlobalPriorityActiveLocked()) {
dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
asSystemService, keyEvent, stream, musicOnly);
} else {
// TODO: Consider the case when both volume up and down keys are pressed
// at the same time.
mVolumeKeyEventHandler.handleVolumeKeyEventLocked(packageName, pid, uid,
asSystemService, keyEvent, opPackageName, stream, musicOnly);
}
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
大部分情况,我们会执行**mVolumeKeyEventHandler.handleVolumeKeyEventLocked(packageName, pid, uid,asSystemService, keyEvent, opPackageName, stream, musicOnly);
**我们接着进行跟踪handleVolumeKeyEventLocked方法
void handleVolumeKeyEventLocked(String packageName, int pid, int uid,
boolean asSystemService, KeyEvent keyEvent, String opPackageName, int stream,
boolean musicOnly) {
handleKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent, false,
opPackageName, stream, musicOnly);
}
void handleKeyEventLocked(String packageName, int pid, int uid,
boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock,
String opPackageName, int stream, boolean musicOnly) {
if (keyEvent.isCanceled()) {
return;
}
int overriddenKeyEvents = 0;
if (mCustomMediaKeyDispatcher != null
&& mCustomMediaKeyDispatcher.getOverriddenKeyEvents() != null) {
overriddenKeyEvents = mCustomMediaKeyDispatcher.getOverriddenKeyEvents()
.get(keyEvent.getKeyCode());
}
cancelTrackingIfNeeded(packageName, pid, uid, asSystemService, keyEvent,
needWakeLock, opPackageName, stream, musicOnly, overriddenKeyEvents);
if (!needTracking(keyEvent, overriddenKeyEvents)) {
if (mKeyType == KEY_TYPE_VOLUME) {
dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
asSystemService, keyEvent, stream, musicOnly);
} else {
dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
keyEvent, needWakeLock);
}
return;
}
if (isFirstDownKeyEvent(keyEvent)) {
mTrackingFirstDownKeyEvent = keyEvent;
mIsLongPressing = false;
return;
}
//处理长按
// Long press is always overridden here, otherwise the key event would have been
// already handled
if (isFirstLongPressKeyEvent(keyEvent)) {
mIsLongPressing = true;
}
if (mIsLongPressing) {
handleLongPressLocked(keyEvent, needWakeLock, overriddenKeyEvents);
return;
}
if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
mTrackingFirstDownKeyEvent = null;
if (shouldTrackForMultipleTapsLocked(overriddenKeyEvents)) {
if (mMultiTapCount == 0) {
mMultiTapTimeoutRunnable = createSingleTapRunnable(packageName, pid,
uid, asSystemService, keyEvent, needWakeLock,
opPackageName, stream, musicOnly,
isSingleTapOverridden(overriddenKeyEvents));
if (isSingleTapOverridden(overriddenKeyEvents)
&& !isDoubleTapOverridden(overriddenKeyEvents)
&& !isTripleTapOverridden(overriddenKeyEvents)) {
mMultiTapTimeoutRunnable.run();
} else {
mHandler.postDelayed(mMultiTapTimeoutRunnable,
MULTI_TAP_TIMEOUT);
mMultiTapCount = 1;
mMultiTapKeyCode = keyEvent.getKeyCode();
}
} else if (mMultiTapCount == 1) {
mHandler.removeCallbacks(mMultiTapTimeoutRunnable);
mMultiTapTimeoutRunnable = createDoubleTapRunnable(packageName, pid,
uid, asSystemService, keyEvent, needWakeLock, opPackageName,
stream, musicOnly, isSingleTapOverridden(overriddenKeyEvents),
isDoubleTapOverridden(overriddenKeyEvents));
if (isTripleTapOverridden(overriddenKeyEvents)) {
mHandler.postDelayed(mMultiTapTimeoutRunnable, MULTI_TAP_TIMEOUT);
mMultiTapCount = 2;
} else {
mMultiTapTimeoutRunnable.run();
}
} else if (mMultiTapCount == 2) {
mHandler.removeCallbacks(mMultiTapTimeoutRunnable);
onTripleTap(keyEvent);
}
} else {
dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
keyEvent, needWakeLock, opPackageName, stream, musicOnly);
}
}
}
可以看到上面分出了很多个点击次数分支,单击的时候走什么逻辑,双击的时候走什么逻辑,三击的时候走什么逻辑,但是随着逻辑的往下跟,发现他们最后都会指定到*dispatchDownAndUpKeyEventsLocked
*,我们着重看一下这个方法的实现。
private void dispatchDownAndUpKeyEventsLocked(String packageName, int pid, int uid,
boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock,
String opPackageName, int stream, boolean musicOnly) {
KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
if (mKeyType == KEY_TYPE_VOLUME) {
//调节音量走这
dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
asSystemService, downEvent, stream, musicOnly);
dispatchVolumeKeyEventLocked(packageName, opPackageName, pid, uid,
asSystemService, keyEvent, stream, musicOnly);
} else {
dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService, downEvent,
needWakeLock);
dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent,
needWakeLock);
}
}
private void dispatchVolumeKeyEventLocked(String packageName, String opPackageName, int pid,
int uid, boolean asSystemService, KeyEvent keyEvent, int stream,
boolean musicOnly) {
boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
int direction = 0;
boolean isMute = false;
//根据不同的keycode设置相对应的direction
switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_VOLUME_UP:
direction = AudioManager.ADJUST_RAISE;
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
direction = AudioManager.ADJUST_LOWER;
break;
case KeyEvent.KEYCODE_VOLUME_MUTE:
isMute = true;
break;
}
if (down || up) {
//根据事件类型(按下或抬起),设置不同的标志。
int flags = AudioManager.FLAG_FROM_KEY;
if (!musicOnly) {
// These flags are consistent with the home screen
if (up) {
flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
} else {
flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
}