Android 10 Android音量调节(一)

简介: 学习笔记

在手机和平板上面,我们实际上能调节的也就5个音量:

流类型 最大音量 音量 默认音量 含义
STREAM_VOICE_CALL 5 1 4 通话音量
STREAM_RING 7 0 5 铃声,通知音量等
STREAM_MUSIC 15 0 5 多媒体音量
STREAM_ALARM 7 0 6 闹钟音量
STREAM_BLUETOOTH_SCO 15 0 7 蓝牙音量

1 按键的处理流程

// frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
// pre-condition: event.getKeyCode() is one of KeyEvent.KEYCODE_VOLUME_UP,
    //          KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE
    private void dispatchDirectAudioEvent(KeyEvent event) {
        // When System Audio Mode is off, volume keys received by AVR can be either consumed by AVR
        // or forwarded to the TV. It's up to Amplifier manufacturer’s implementation.
        HdmiControlManager hdmiControlManager = getHdmiControlManager();
        if (null != hdmiControlManager
                && !hdmiControlManager.getSystemAudioMode()
                && shouldCecAudioDeviceForwardVolumeKeysSystemAudioModeOff()) {
            HdmiAudioSystemClient audioSystemClient = hdmiControlManager.getAudioSystemClient();
            if (audioSystemClient != null) {
                audioSystemClient.sendKeyEvent(
                        event.getKeyCode(), event.getAction() == KeyEvent.ACTION_DOWN);
                return;
            }
        }
        try {
            getAudioService().handleVolumeKey(event, mUseTvRouting,
                    mContext.getOpPackageName(), TAG);
        } catch (Exception e) {
            Log.e(TAG, "Error dispatching volume key in handleVolumeKey for event:"
                    + event, e);
        }
    }
   // 这里通过AIDL获取IAudioService的实例,
    static IAudioService getAudioService() {
        IAudioService audioService = IAudioService.Stub.asInterface(
                ServiceManager.checkService(Context.AUDIO_SERVICE));
        if (audioService == null) {
            Log.w(TAG, "Unable to find IAudioService interface.");
        }
        return audioService;
    }

在这里,通过Binder获取到了AudioService的实例,调用AudioService的handleVolumeKey()方法。

// pre-condition: event.getKeyCode() is one of KeyEvent.KEYCODE_VOLUME_UP,
    //                                   KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE
    public void handleVolumeKey(@NonNull KeyEvent event, boolean isOnTv,
            @NonNull String callingPackage, @NonNull String caller) {
        int keyEventMode = VOL_ADJUST_NORMAL;
        if (isOnTv) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                keyEventMode = VOL_ADJUST_START;
            } else { // may catch more than ACTION_UP, but will end vol adjustement
                // the vol key is either released (ACTION_UP), or multiple keys are pressed
                // (ACTION_MULTIPLE) and we don't know what to do for volume control on CEC, end
                // the repeated volume adjustement
                keyEventMode = VOL_ADJUST_END;
            }
        } else if (event.getAction() != KeyEvent.ACTION_DOWN) {
            return;
        }
        int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND
                | AudioManager.FLAG_FROM_KEY;
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_VOLUME_UP:
                    adjustSuggestedStreamVolume(AudioManager.ADJUST_RAISE,
                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, callingPackage, caller,
                            Binder.getCallingUid(), true, keyEventMode);
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                    adjustSuggestedStreamVolume(AudioManager.ADJUST_LOWER,
                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, callingPackage, caller,
                            Binder.getCallingUid(), true, keyEventMode);
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                    adjustSuggestedStreamVolume(AudioManager.ADJUST_TOGGLE_MUTE,
                            AudioManager.USE_DEFAULT_STREAM_TYPE, flags, callingPackage, caller,
                            Binder.getCallingUid(), true, VOL_ADJUST_NORMAL);
                }
                break;
            default:
                Log.e(TAG, "Invalid key code " + event.getKeyCode() + " sent by " + callingPackage);
                return; // not needed but added if code gets added below this switch statement
        }
    }

在handleVolumeKey()方法里面,调用了adjustSuggestedStreamVolume方法,参数含义如下:

按键类型 Audio Service操作类型 含义
KEYCODE_VOLUME_UP AudioManager.ADJUST_RAISE 音量减
KEYCODE_VOLUME_DOWN AudioManager.ADJUST_LOWER 音量加
KEYCODE_VOLUME_MUTE AudioManager.ADJUST_TOGGLE_MUTE 改变静音状态

在按键的处理过程中,并没有将相应的code传递给AudioService,而是使用了相关的定义,将KEYCODE_VOLUME_UP等操作转化为了ADJUST_RAISE等。而flag存储了一些对音量的要求或者信息。

// frameworks/base/media/java/android/media/AudioManager.java
    /**
     * Increase the ringer volume.
     */
    public static final int ADJUST_RAISE = 1;
    /**
     * Decrease the ringer volume.
     */
    public static final int ADJUST_LOWER = -1;
    /**
     * Maintain the previous ringer volume. This may be useful when needing to
     * show the volume toast without actually modifying the volume.
     *
     */
    public static final int ADJUST_SAME = 0;
    /**
     * Mute the volume. Has no effect if the stream is already muted.
     */
    public static final int ADJUST_MUTE = -100;
    /**
     * Unmute the volume. Has no effect if the stream is not muted.
     */
    public static final int ADJUST_UNMUTE = 100;
    /**
     * Toggle the mute state. If muted the stream will be unmuted. If not muted
     * the stream will be muted.
     */
    public static final int ADJUST_TOGGLE_MUTE = 101;

2 adjustSuggestedStreamVolume

接下来就到了AudioService的adjustSuggestedStreamVolume方法里面了:

private void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
            String callingPackage, String caller, int uid) {
        //   ...省略部分代码
        final int streamType;
        if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
            streamType = mVolumeControlStream;
        } else {
            // 这里获取到,可能是活动状态的音频流,但是不确定,还有待进一步确认
            final int maybeActiveStreamType = getActiveStreamType(suggestedStreamType);  
            final boolean activeForReal;
            if (maybeActiveStreamType == AudioSystem.STREAM_MUSIC) {
                // 这里如果STREAM_MUSIC现在在AudioFlinger处理的流中或在最后的0ms中处于活动状态,则为true。
                activeForReal = isAfMusicActiveRecently(0);
            } else {
                // 会调用native方法,不深究
                activeForReal = AudioSystem.isStreamActive(maybeActiveStreamType, 0);
            }
            // 如果activeForReal为true或者mVolumeControlStream为-1
            // 那么确定要操作修改的流类型为maybeActiveStreamType对应的流类型
            if (activeForReal || mVolumeControlStream == -1) {
                streamType = maybeActiveStreamType;
            } else {
                // activeForReal为false并且mVolumeControlStream不为-1
                // 表示用户点击了音量进度条,这时候要操作修改的流类型为mVolumeControlStream对应的流类型
                streamType = mVolumeControlStream;
            }
        }
        final boolean isMute = isMuteAdjust(direction);
        // 确保我们获取到的流类型是有效的
        ensureValidStreamType(streamType);
        // 将我们获取到的流,进行流映射,拿到最终需要操作的流类型
        final int resolvedStream = mStreamVolumeAlias[streamType];
        // Play sounds on STREAM_RING only.
        if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
                resolvedStream != AudioSystem.STREAM_RING) {
            flags &= ~AudioManager.FLAG_PLAY_SOUND;
        }
        // For notifications/ring, show the ui before making any adjustments
        // Don't suppress mute/unmute requests
        // 通知和响铃,调整音量之前先显示UI。
        if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)) {
            // 将操作置为ADJUST_SAME(ADJUST_SAME = 0);
            direction = 0;
            flags &= ~AudioManager.FLAG_PLAY_SOUND;
            flags &= ~AudioManager.FLAG_VIBRATE;
            if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment");
        }
        // 这里设置音量
        adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid);
    }

这里需要注意的有以下几点:

  1、mUserSelectedVolumeControlStream:这个属性表示,用户是否已通过单击音量进度条选择音量流来更改由音量键控制的音量,如果mVolumeControlStream为-1,那么mUserSelectedVolumeControlStream 为false。说简单点,当用户点击了某个音量条,这时再去按下音量加减,这个时候调节的是你点击的那个流类型。

  2、getActiveStreamType:获取我们要控制的流的类型,当然,只是可能需要控制的流类型,还需要进一步确认。

  3、mStreamVolumeAlias[streamType]:进行流映射,获取最终需要调整的流类型

  4、suppressAdjustment:字面意思为抑制调整,为什么抑制调整呢,说白了,当我们没有显示音量的UI进度条的时候,不管我们是加音量还是减音量(注意:静音和解静音除外),这个时候都是先显示音量条,而不去改变音量的大小。所以当这个方法返回true的时候, direction = 0,这里direction为0就表示我们的操作为ADJUST_SAME,大家可以在AudioManager里面查看ADJUST_SAME的注释就知道这个操作表示只弹出UI但是不调整音量大小。

  5、adjustStreamVolume:进行音量的调整

3 adjustStreamVolume

进入到了adjustStreamVolume函数,该函数比较长,得分片段分析。

private void adjustStreamVolume(int streamType, int direction, int flags,
            String callingPackage, String caller, int uid) {
        if (mUseFixedVolume) {
            return;
        }
        ensureValidDirection(direction);
        ensureValidStreamType(streamType);
        boolean isMuteAdjust = isMuteAdjust(direction);
        if (isMuteAdjust && !isStreamAffectedByMute(streamType)) {
            return;
        }
        // use stream type alias here so that streams with same alias have the same behavior,
        // including with regard to silent mode control (e.g the use of STREAM_RING below and in
        // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
        int streamTypeAlias = mStreamVolumeAlias[streamType];
        VolumeStreamState streamState = mStreamStates[streamTypeAlias];
        ......
}
  • mUseFixedVolume:表示使用固定音量,我们无法修改音量
  • mStreamVolumeAlias[streamType]:进行音频流的映射,拿到映射后的音频流
  • mStreamStates[streamTypeAlias]:mStreamStates是一个存储VolumeStreamState类型的数组,保存着每个音频流的状态。VolumeStreamState是AudioService的一个内部类,里面保存单个音频流的所有信息,比如流类型,音量大小,mute状态等。并且相同的流类型,在不同的设备,大小也是不一样的(比如耳机和扬声器,媒体音量大小是不一样的),这也是在VolumeStreamState里面去维护的。)

private void adjustStreamVolume(int streamType, int direction, int flags,
        String callingPackage, String caller, int uid) {
        ......
        final int device = getDeviceForStream(streamTypeAlias);
        int aliasIndex = streamState.getIndex(device);
        boolean adjustVolume = true;
        int step;
        // skip a2dp absolute volume control request when the device
        // is not an a2dp device
        // 如果不是蓝牙设备,则跳过音量绝对控制请求 
        if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 &&
            (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
            return;
        }
        // If we are being called by the system (e.g. hardware keys) check for current user
        // so we handle user restrictions correctly.
        // uid判断,方便去做用户权限处理
        if (uid == android.os.Process.SYSTEM_UID) {
            uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
        }
        // 权限处理
        if (mAppOps.noteOp(STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage)
                != AppOpsManager.MODE_ALLOWED) {
            return;
        }
        .......
    }
  • getDeviceForStream:通过流类型去获取设备类型这里去获取设备类型,通过流类型获取到了对应的VolumeStreamState的实例,然后调用了其observeDevicesForStream_syncVSS方法去获取devices,在observeDevicesForStream_syncVSS方法里面,又会去调用AudioSystem的getDevicesForStream去获取设备,这是个native方法。

private void adjustStreamVolume(int streamType, int direction, int flags,
        String callingPackage, String caller, int uid) {
        ......
        // 清除掉任何待处理的音量命令
        synchronized (mSafeMediaVolumeState) {
            mPendingVolumeCommand = null;
        }
        // 表示不是固定音量 
        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
        // 如果是多媒体音量,并且是使用固定音量的设备
        if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) &&
               ((device & mFixedVolumeDevices) != 0)) {
            // 加上表示固定音量的flag
            flags |= AudioManager.FLAG_FIXED_VOLUME;
            // Always toggle between max safe volume and 0 for fixed volume devices where safe
            // volume is enforced, and max and 0 for the others.
            // This is simulated by stepping by the full allowed volume range
            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
                    (device & mSafeMediaVolumeDevices) != 0) {
                step = safeMediaVolumeIndex(device);
            } else {
                step = streamState.getMaxIndex();
            }
            if (aliasIndex != 0) {
                aliasIndex = step;
            }
        } else {
            // convert one UI step (+/-1) into a number of internal units on the stream alias
            // 如果不是多媒体音量,或者是多媒体音量但是不是固定音量的设备时
            // 将音量值的步进量从源流类型变换到目标流类型下,由于不同的流类型的音量调节范围不同,所以这个转换是必需的
            step = rescaleIndex(10, streamType, streamTypeAlias);
        }
        // 情景模式的处理
        // If either the client forces allowing ringer modes for this adjustment,
        // or the stream type is one that is affected by ringer modes
        if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
                (streamTypeAlias == getUiSoundsStreamType())) {
            int ringerMode = getRingerModeInternal();
            // do not vibrate if already in vibrate mode
            // 如果已经是震动模式,则不进行震动
            if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
                flags &= ~AudioManager.FLAG_VIBRATE;
            }
            // Check if the ringer mode handles this adjustment. If it does we don't
            // need to adjust the volume further.
            // 根据我们的操作来检查是否需要切换情景模式
            final int result = checkForRingerModeChange(aliasIndex, direction, step,
                    streamState.mIsMuted, callingPackage, flags);
            adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0;
            // If suppressing a volume adjustment in silent mode, display the UI hint
            if ((result & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) {
                flags |= AudioManager.FLAG_SHOW_SILENT_HINT;
            }
            // If suppressing a volume down adjustment in vibrate mode, display the UI hint
            if ((result & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) {
                flags |= AudioManager.FLAG_SHOW_VIBRATE_HINT;
            }
        }
        // If the ringermode is suppressing media, prevent changes
        // 勿扰模式
        if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
            adjustVolume = false;
        }
        // 获取旧的音量大小
        int oldIndex = mStreamStates[streamType].getIndex(device);
        ......
    }
  • 固定音量设备的处理
  • 音量步进的处理:rescaleIndex方法,将音量值的步进量从源流类型变换到目标流类型下,由于不同的流类型的音量调节范围不同,所以这个转换是必需的。计算按下音量键的音量步进值。这个步进值是10而不是1。在VolumeStreamState中保存的音量值是其实际值的10倍,这是为了在不同流类型之间进行音量转化时能够保证一定精度的一种实现。可以理解为在转化过程中保留了小数点后一位的精度。
  • 情景模式处理:情景模式的处理就涉及到了音量的调整,以及情景模式的切换,在切换情景模式(震动到响铃除外)的时候,是没有去调整音量的。通过adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0这一句体现出来的。
  • 勿扰模式处理:

private void adjustStreamVolume(int streamType, int direction, int flags,
        String callingPackage, String caller, int uid) {
        ......
        if (adjustVolume
                && (direction != AudioManager.ADJUST_SAME) && (keyEventMode != VOL_ADJUST_END)) {
            mAudioHandler.removeMessages(MSG_UNMUTE_STREAM);
            // 处理静音调整
            if (isMuteAdjust) {
                boolean state;
                if (direction == AudioManager.ADJUST_TOGGLE_MUTE) {
                    state = !streamState.mIsMuted;
                } else {
                    state = direction == AudioManager.ADJUST_MUTE;
                }
                if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
                    setSystemAudioMute(state);
                }
                for (int stream = 0; stream < mStreamStates.length; stream++) {
                    if (streamTypeAlias == mStreamVolumeAlias[stream]) {
                        if (!(readCameraSoundForced()
                                    && (mStreamStates[stream].getStreamType()
                                        == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
                            // 这里获取当前流对应的VolumeStreamState实例,然后去调用mute方法
                            // 最终也会到AudioSystem去调用native方法
                            mStreamStates[stream].mute(state);
                        }
                    }
                }
            } else if ((direction == AudioManager.ADJUST_RAISE) &&
                    !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
                // 安全音量提示,音量增加的时候才会去检测
                Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
                mVolumeController.postDisplaySafeVolumeWarning(flags);
            } else if (!isFullVolumeDevice(device)
                    && (streamState.adjustIndex(direction * step, device, caller,
                            hasModifyAudioSettings)
                            || streamState.mIsMuted)) {
                // Post message to set system volume (it in turn will post a
                // message to persist).
                if (streamState.mIsMuted) {
                    // Unmute the stream if it was previously muted
                    if (direction == AudioManager.ADJUST_RAISE) {
                        // unmute immediately for volume up
                        streamState.mute(false);
                    } else if (direction == AudioManager.ADJUST_LOWER) {
                        if (mIsSingleVolume) {
                            sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
                                    streamTypeAlias, flags, null, UNMUTE_STREAM_DELAY);
                        }
                    }
                }
                // 设置音量到底层
                sendMsg(mAudioHandler,
                        MSG_SET_DEVICE_VOLUME,
                        SENDMSG_QUEUE,
                        device,
                        0,
                        streamState,
                        0);
            }
            int newIndex = mStreamStates[streamType].getIndex(device);
            // hdmi音量更新,这个不过多讲解
            // Check if volume update should be send to AVRCP
            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                    && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                    && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
                if (DEBUG_VOL) {
                    Log.d(TAG, "adjustSreamVolume: postSetAvrcpAbsoluteVolumeIndex index="
                            + newIndex + "stream=" + streamType);
                }
                mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(newIndex / 10);
            }
            // Check if volume update should be send to Hearing Aid
            if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
                // only modify the hearing aid attenuation when the stream to modify matches
                // the one expected by the hearing aid
                if (streamType == getHearingAidStreamType()) {
                    if (DEBUG_VOL) {
                        Log.d(TAG, "adjustSreamVolume postSetHearingAidVolumeIndex index="
                                + newIndex + " stream=" + streamType);
                    }
                    mDeviceBroker.postSetHearingAidVolumeIndex(newIndex, streamType);
                }
            }
            // Check if volume update should be sent to Hdmi system audio.
            if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
                setSystemAudioVolume(oldIndex, newIndex, getStreamMaxVolume(streamType), flags);
            }
        }
        final int newIndex = mStreamStates[streamType].getIndex(device);
           if (adjustVolume) {
            synchronized (mHdmiClientLock) {
                if (mHdmiManager != null) {
                    // mHdmiCecSink true => mHdmiPlaybackClient != null
                    if (mHdmiCecSink
                            && mHdmiCecVolumeControlEnabled
                            && streamTypeAlias == AudioSystem.STREAM_MUSIC
                            // vol change on a full volume device
                            && isFullVolumeDevice(device)) {
                        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
                        switch (direction) {
                            case AudioManager.ADJUST_RAISE:
                                keyCode = KeyEvent.KEYCODE_VOLUME_UP;
                                break;
                            case AudioManager.ADJUST_LOWER:
                                keyCode = KeyEvent.KEYCODE_VOLUME_DOWN;
                                break;
                            case AudioManager.ADJUST_TOGGLE_MUTE:
                                keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
                                break;
                            default:
                                break;
                        }
                        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
                            final long ident = Binder.clearCallingIdentity();
                            try {
                                final long time = java.lang.System.currentTimeMillis();
                                switch (keyEventMode) {
                                    case VOL_ADJUST_NORMAL:
                                        mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, true);
                                        mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, false);
                                        break;
                                    case VOL_ADJUST_START:
                                        mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, true);
                                        break;
                                    case VOL_ADJUST_END:
                                        mHdmiPlaybackClient.sendVolumeKeyEvent(keyCode, false);
                                        break;
                                    default:
                                        Log.e(TAG, "Invalid keyEventMode " + keyEventMode);
                                }
                            } finally {
                                Binder.restoreCallingIdentity(ident);
                            }
                        }
                    }
                    if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                            && (oldIndex != newIndex || isMuteAdjust)) {
                        maybeSendSystemAudioStatusCommand(isMuteAdjust);
                    }
                }
            }
        }
        // 通知外界音量发生变化 
        sendVolumeUpdate(streamType, oldIndex, newIndex, flags, device);
    }

到这里,音量键分析完毕。

相关文章
|
开发工具 Android开发 iOS开发
Android、iOS平台RTMP/RTSP播放器实现实时音量调节
介绍移动端RTMP、RTSP播放器实时音量调节之前,我们之前也写过,为什么windows播放端加这样的接口,windows端播放器在多窗口大屏显示的场景下尤其需要,尽管我们老早就有了实时静音接口,相对实时静音来说,播放端实时音量调节粒度更细,从[0, 100],用户体验更好。
190 1
|
5月前
|
XML Java Android开发
Android App开发音量调节中实现拖动条和滑动条和音频管理器AudioManager讲解及实战(超详细 附源码和演示视频)
Android App开发音量调节中实现拖动条和滑动条和音频管理器AudioManager讲解及实战(超详细 附源码和演示视频)
222 0
|
Android开发 开发者
Android平台GB28181设备接入端语音广播如何实现实时音量调节
Android平台GB28181设备接入,语音广播功能非常重要,本文要介绍的,不是语音广播的流程,语音广播流程,之前的blog也有非常详细的分享,感兴趣的可以参考官方规范书的交互流程:
|
XML Java Android开发
Android6.0 源码修改之Settings音量调节界面增加通话音量调节
Android6.0 源码修改之Settings音量调节界面增加通话音量调节
86 0
|
Android开发 Java
Android6.0 源码修改之Settings音量调节界面增加通话音量调节
Android6.0 源码修改之Settings音量调节界面增加通话音量调节前言今天客户提了个需求,因为我们的设备在正常情况下无法调节通话音量,只有在打电话过程中,按物理音量加减键才能出现调节通话音量seekBar,很不方便,于是乎需求就来了。
1318 0
|
7天前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
20天前
|
Android开发 开发者 Kotlin
探索安卓开发中的新特性
【9月更文挑战第14天】本文将引导你深入理解安卓开发领域的一些最新特性,并为你提供实用的代码示例。无论你是初学者还是经验丰富的开发者,这篇文章都会给你带来新的启示和灵感。让我们一起探索吧!
|
4天前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
21 7
|
7天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台解决方案
【9月更文挑战第27天】在移动应用开发的广阔天地中,安卓和iOS两大操作系统如同双子星座般耀眼。开发者们在这两大平台上追逐着创新的梦想,却也面临着选择的难题。如何在保持高效的同时,实现跨平台的开发?本文将带你探索跨平台开发的魅力所在,揭示其背后的技术原理,并通过实际案例展示其应用场景。无论你是安卓的忠实拥趸,还是iOS的狂热粉丝,这篇文章都将为你打开一扇通往跨平台开发新世界的大门。
|
4天前
|
缓存 Java Linux
探索安卓开发:从新手到专家的旅程
【9月更文挑战第30天】在这篇文章中,我们将一起踏上一段激动人心的旅程,探索安卓开发的广阔世界。无论你是刚入门的新手,还是希望提升技能的开发者,本文都将为你提供宝贵的知识和指导。我们将深入探讨安卓开发的基础知识、关键概念、实用工具和最佳实践,帮助你在安卓开发领域取得更大的成功。让我们一起开启这段精彩的旅程吧!
下一篇
无影云桌面