Android 音量调节流程分析

时间:2024-11-01 07:06:07

音量调节流程分析

img

  • 按下音量键

音量键被按下后,按键事件会一路派发给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:

  • 这个方法将音量键事件派发到与指定流类型相关的媒体会话。它不依赖于具体的媒体会话,而是根据流类型进行处理,适用于更广泛的音量事件派发场景。

简单举个例子就是:

假设你正在开发一个视频播放器应用,用户在观看视频时可以使用音量键来调整音量。我们将在不同情况下处理音量键事件。

  1. 使用 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.");
}

  1. 使用 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;
                    }