IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android T CEC AVC Feature -> 正文阅读

[移动开发]Android T CEC AVC Feature

AVC是android T 在cec部分添加的一个feature。以盒子为例,它会在playback device cec状态更新时尝试去确认TV是否是支持<SET AUDIO VOLUME LEVEL>的,如果支持就更新该设备的avc flag,并在音量调节时通知TV进行音量更新。

和以前版本的区别,目前看就是在直接进行音量更新setStreamVolume时会发送<SET AUDIO VOLUME LEVEL> message进行直接更新,但是adjustStreamVolume仍然还是依赖<User Control Pressed>,这样的话作用就大打折扣。鉴于目前支持这个消息的机器应该也是几乎没有,暂时看意义比较有限。此外,android为了支持这个feature,在playback内部也增加了DeviceDiscovery的设计,也增加了cec bus的负担。

1.开机时就向AudioDeviceVolumeManager注册了OnDeviceVolumeBehaviorChangedListener

HdmiControlService.java

    @Override
    public void onBootPhase(int phase) {
        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
            mDisplayManager = getContext().getSystemService(DisplayManager.class);
            mTvInputManager = (TvInputManager) getContext().getSystemService(
                    Context.TV_INPUT_SERVICE);
            mPowerManager = new PowerManagerWrapper(getContext());
            mPowerManagerInternal = new PowerManagerInternalWrapper();
            mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
            mStreamMusicMaxVolume = getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
            if (mAudioDeviceVolumeManager == null) {
                mAudioDeviceVolumeManager =
                        new AudioDeviceVolumeManagerWrapper(getContext());
            }
            getAudioDeviceVolumeManager().addOnDeviceVolumeBehaviorChangedListener(
                    mServiceThreadExecutor, this::onDeviceVolumeBehaviorChanged);
        } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
            runOnServiceThread(this::bootCompleted);
        }
    }
    /**
     * @hide
     * Interface definition of a callback to be invoked when the volume behavior of an audio device
     * is updated.
     */
    public interface OnDeviceVolumeBehaviorChangedListener {
        /**
         * Called on the listener to indicate that the volume behavior of a device has changed.
         * @param device the audio device whose volume behavior changed
         * @param volumeBehavior the new volume behavior of the audio device
         */
        void onDeviceVolumeBehaviorChanged(
                @NonNull AudioDeviceAttributes device,
                @AudioManager.DeviceVolumeBehavior int volumeBehavior);
    }

这里后面再分析。

    /**
     * Listener for changes to the volume behavior of an audio output device. Caches the
     * volume behavior of devices used for Absolute Volume Control.
     */
    @VisibleForTesting
    @ServiceThreadOnly
    void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) {
        assertRunOnServiceThread();
        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
            synchronized (mLock) {
                mAudioDeviceVolumeBehaviors.put(device, volumeBehavior);
            }
            checkAndUpdateAbsoluteVolumeControlState();
        }
    }
    // Audio output devices used for Absolute Volume Control
    private static final List<AudioDeviceAttributes> AVC_AUDIO_OUTPUT_DEVICES =
            Collections.unmodifiableList(Arrays.asList(AUDIO_OUTPUT_DEVICE_HDMI,
                    AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC));

PLAYBACK相关的AUDIO_OUTPUT_DEVICE_HDMI,TV类型的AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC都属于AVC设备。

当系统的AVC设备对应的volume behaviour发生变更时,就会通过checkAndUpdateAbsoluteVolumeControlState来检查和更新HdmiControlService内部的状态。

2. 进行volume control behavior更新的时间。

监听cec status的设计在T上这里的实现再次发生变更,直接通过DEVICE_OUT_HDMI的volume behaviour的方式来更新cec sink device的状态。

首先如果DEVICE_OUT_HDMI设备有过AudioService的setDeviceVolumeBehavior接口设置过behaviour的话,就不再更新了。

这里有一个问题,如果cec不可用了,然后也设置过FULL的behaviour的话,盒子的音量键将完全失去作用。

    //==========================================================================================
    // Hdmi CEC:
    // - System audio mode:
    //     If Hdmi Cec's system audio mode is on, audio service should send the volume change
    //     to HdmiControlService so that the audio receiver can handle it.
    // - CEC sink:
    //     OUT_HDMI becomes a "full volume device", i.e. output is always at maximum level
    //     and volume changes won't be taken into account on this device. Volume adjustments
    //     are transformed into key events for the HDMI playback client.
    //==========================================================================================

    @GuardedBy("mHdmiClientLock")
    private void updateHdmiCecSinkLocked(boolean hdmiCecSink) {
        if (!hasDeviceVolumeBehavior(AudioSystem.DEVICE_OUT_HDMI)) {
            if (hdmiCecSink) {
                if (DEBUG_VOL) {
                    Log.d(TAG, "CEC sink: setting HDMI as full vol device");
                }
                setDeviceVolumeBehaviorInternal(
                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
                        AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL,
                        "AudioService.updateHdmiCecSinkLocked()");
            } else {
                if (DEBUG_VOL) {
                    Log.d(TAG, "TV, no CEC: setting HDMI as regular vol device");
                }
                // Android TV devices without CEC service apply software volume on
                // HDMI output
                setDeviceVolumeBehaviorInternal(
                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
                        AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE,
                        "AudioService.updateHdmiCecSinkLocked()");
            }
            postUpdateVolumeStatesForAudioDevice(AudioSystem.DEVICE_OUT_HDMI,
                    "HdmiPlaybackClient.DisplayStatusCallback");
        }
    }

只有persistDeviceVolumeBehavior才会将volume behaviour记录到system settings里面,比如

_id:36 name:AudioService_DeviceVolumeBehavior_hdmi pkg:android value:1 default:1 defaultSystemSet:true

    public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device,
            @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior, @Nullable String pkgName) {
        // verify permissions
        enforceModifyAudioRoutingPermission();
        // verify arguments
        Objects.requireNonNull(device);
        AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
        sVolumeLogger.log(new AudioEventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
                + device.getAddress() + " behavior:"
                + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
                + " pack:" + pkgName).printLog(TAG));
        if (pkgName == null) {
            pkgName = "";
        }
        if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
            avrcpSupportsAbsoluteVolume(device.getAddress(),
                    deviceVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
            return;
        }

        setDeviceVolumeBehaviorInternal(device, deviceVolumeBehavior, pkgName);
        persistDeviceVolumeBehavior(device.getInternalType(), deviceVolumeBehavior);
    }

AudioService在设置MyHdmiControlStatusChangeListenerCallback时必然会调用updateHdmiCecSinkLocked来更新一次volume behaviour。

    @VisibleForTesting
    void addHdmiControlStatusChangeListener(
            final IHdmiControlStatusChangeListener listener) {
        final HdmiControlStatusChangeListenerRecord record =
                new HdmiControlStatusChangeListenerRecord(listener);
        try {
            listener.asBinder().linkToDeath(record, 0);
        } catch (RemoteException e) {
            Slog.w(TAG, "Listener already died");
            return;
        }
        synchronized (mLock) {
            mHdmiControlStatusChangeListenerRecords.add(record);
        }

        // Inform the listener of the initial state of each HDMI port by generating
        // hotplug events.
        runOnServiceThread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    if (!mHdmiControlStatusChangeListenerRecords.contains(record)) return;
                }

                // Return the current status of mHdmiControlEnabled;
                synchronized (mLock) {
                    invokeHdmiControlStatusChangeListenerLocked(listener, mHdmiControlEnabled);
                }
            }
        });
    }

    private class MyHdmiControlStatusChangeListenerCallback
            implements HdmiControlManager.HdmiControlStatusChangeListener {
        public void onStatusChange(@HdmiControlManager.HdmiCecControl int isCecEnabled,
                boolean isCecAvailable) {
            synchronized (mHdmiClientLock) {
                if (mHdmiManager == null) return;
                boolean cecEnabled = isCecEnabled == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED;
                updateHdmiCecSinkLocked(cecEnabled ? isCecAvailable : false);
            }
        }
    };

以Playback为例,当连接TV的CEC ready时,则设置behaviour为DEVICE_VOLUME_BEHAVIOUR_FULL,这里仅仅是将DEVICE_HDMI_OUT添加到full device列表里面,如

? mFixedVolumeDevices=0x800,0x200000
? mFullVolumeDevices=0x400,0x40000,0x40001
? mAbsoluteVolumeDevices.keySet()=

    private void setDeviceVolumeBehaviorInternal(@NonNull AudioDeviceAttributes device,
            @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior, @NonNull String caller) {
        int audioSystemDeviceOut = device.getInternalType();
        boolean volumeBehaviorChanged = false;
        // update device masks based on volume behavior
        switch (deviceVolumeBehavior) {
            case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE:
                volumeBehaviorChanged |=
                        removeAudioSystemDeviceOutFromFullVolumeDevices(audioSystemDeviceOut)
                        | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut)
                        | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut)
                                != null);
                break;
            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED:
                volumeBehaviorChanged |=
                        removeAudioSystemDeviceOutFromFullVolumeDevices(audioSystemDeviceOut)
                        | addAudioSystemDeviceOutToFixedVolumeDevices(audioSystemDeviceOut)
                        | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut)
                                != null);
                break;
            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL:
                volumeBehaviorChanged |=
                        addAudioSystemDeviceOutToFullVolumeDevices(audioSystemDeviceOut)
                        | removeAudioSystemDeviceOutFromFixedVolumeDevices(audioSystemDeviceOut)
                        | (removeAudioSystemDeviceOutFromAbsVolumeDevices(audioSystemDeviceOut)
                                != null);
                break;
            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE:
            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE:
                throw new IllegalArgumentException("Absolute volume unsupported for now");
        }

        if (volumeBehaviorChanged) {
            sendMsg(mAudioHandler, MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR, SENDMSG_QUEUE,
                    deviceVolumeBehavior, 0, device, /*delay*/ 0);
        }

        // log event and caller
        sDeviceLogger.log(new AudioEventLogger.StringEvent(
                "Volume behavior " + deviceVolumeBehavior + " for dev=0x"
                      + Integer.toHexString(audioSystemDeviceOut) + " from:" + caller));
        // make sure we have a volume entry for this device, and that volume is updated according
        // to volume behavior
        postUpdateVolumeStatesForAudioDevice(audioSystemDeviceOut,
                "setDeviceVolumeBehavior:" + caller);
    }

所以每次cec control状态的变更,都会更新mFullVolumeDevices,并且通知IDeviceVolumeBehaviorDispatcher的监听器们进行更新。

    private void dispatchDeviceVolumeBehavior(AudioDeviceAttributes device, int volumeBehavior) {
        final int dispatchers = mDeviceVolumeBehaviorDispatchers.beginBroadcast();
        for (int i = 0; i < dispatchers; i++) {
            try {
                mDeviceVolumeBehaviorDispatchers.getBroadcastItem(i)
                        .dispatchDeviceVolumeBehaviorChanged(device, volumeBehavior);
            } catch (RemoteException e) {
            }
        }
        mDeviceVolumeBehaviorDispatchers.finishBroadcast();
    }

AudioDeviceVolumeManager.java

前面已经有分析过,HdmiControlService在onBootPhase初始化阶段就注册过该监听器。

    private final class DeviceVolumeBehaviorDispatcherStub
            extends IDeviceVolumeBehaviorDispatcher.Stub implements CallbackUtil.DispatcherStub {
        public void register(boolean register) {
            try {
                getService().registerDeviceVolumeBehaviorDispatcher(register, this);
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        }

        @Override
        public void dispatchDeviceVolumeBehaviorChanged(@NonNull AudioDeviceAttributes device,
                @AudioManager.DeviceVolumeBehavior int volumeBehavior) {
            mDeviceVolumeBehaviorChangedListenerMgr.callListeners((listener) ->
                    listener.onDeviceVolumeBehaviorChanged(device, volumeBehavior));
        }
    }

包括开机、hotplug、cec switch change等场景,最终都会进行avc状态的检查。满足进行avc状态检查的一个条件就是AVC_AUDIO_OUT_DEVICE的behaviour必须是FULL或者ABSOLUTE的,而默认情况下只要TV支持CEC这个条件就满足了,所以必然最后会发送<Set Device Volume Level>消息来进行检查。换言之如果cec状态不满足,比如cec关闭,tv不支持cec等场景,那么AVC也就无须考虑了。

    /**
     * Listener for changes to the volume behavior of an audio output device. Caches the
     * volume behavior of devices used for Absolute Volume Control.
     */
    @VisibleForTesting
    @ServiceThreadOnly
    void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) {
        assertRunOnServiceThread();
        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
            synchronized (mLock) {
                mAudioDeviceVolumeBehaviors.put(device, volumeBehavior);
            }
            checkAndUpdateAbsoluteVolumeControlState();
        }
    }

2.checkAndUpdateAbsoluteVolumeControlState

Absolute Volume Control is AVC. 当使用外部音频设备时,并且该音频设备支持<Set Audio Volume Level>时,则可以配置该音频设备支持AVC,并启动相关设置。

目前这里判断外部音频设备是否支持AVC是通过判断是否回复<Feature Abort>来决定的,这值得商榷。

而判断设备自身是否enable avc,则需要满足以下条件。

①TV需要建立system audio control,Playback无此要求。

②CEC使用的AVC audio device相关的volume behaviour必须是DEVICE_VOLUME_BEHAVIOR_FULL? or DEVICE_VOLUME_BEHAVIOR_ABSOLUTE。TV对应的audio device是HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC,而playback对应的则是AUDIO_OUTPUT_DEVICE_HDMI,这俩都是AudioDeviceAttributes。

除了三方应用或者CTS测试可以在通过AudioManager::setDeviceVolumeBehavior接口来设置他们的behaviour之外,一般都是通过AudioService updateCecSink设置的DEVICE_OUT_HDMI behaviour为FULL,这个条件默认也是满足的。

③volume control开关必须是打开的。 T上修改开关是通过HdmiControlManager::setHdmiCecVolumeControlEnabled来实现的,不是之前的版本通过Settings Global了。

④外部的TV/AVR是支持AVC的。目前判断是否支持AVC是通过发送给<Set Volume Level>以后看设备是否回复<Feature Abort>实现的。在CTS测试里面,由于TV的CEC是关闭的,所以这个条件是满足的。

    /**
     * Checks the conditions for Absolute Volume Control (AVC), and enables or disables the feature
     * if necessary. AVC is enabled precisely when a specific audio output device
     * (HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs) is using absolute volume
     * behavior.
     *
     * AVC must be enabled on a Playback device or TV precisely when it is playing
     * audio on an external device (the System Audio device) that supports the feature.
     * This reduces to these conditions:
     *
     * 1. If the System Audio Device is an Audio System: System Audio Mode is active
     * 2. Our HDMI audio output device is using full volume behavior
     * 3. CEC volume is enabled
     * 4. The System Audio device supports AVC (i.e. it supports <Set Audio Volume Level>)
     *
     * If not all of these conditions are met, this method disables AVC if necessary.
     *
     * If all of these conditions are met, this method starts an action to query the System Audio
     * device's audio status, which enables AVC upon obtaining the audio status.
     */
    void checkAndUpdateAbsoluteVolumeControlState() {
        assertRunOnServiceThread();

        // Can't enable or disable AVC before we have access to system services
        if (getAudioManager() == null) {
            return;
        }

        HdmiCecLocalDevice localCecDevice;
        if (isTvDevice() && tv() != null) {
            localCecDevice = tv();
            // Condition 1: TVs need System Audio Mode to be active
            // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the
            // TV is the System Audio Device instead.)
            if (!isSystemAudioActivated()) {
                disableAbsoluteVolumeControl();
                return;
            }
        } else if (isPlaybackDevice() && playback() != null) {
            localCecDevice = playback();
        } else {
            // Either this device type doesn't support AVC, or it hasn't fully initialized yet
            return;
        }

        HdmiDeviceInfo systemAudioDeviceInfo = getHdmiCecNetwork().getSafeCecDeviceInfo(
                localCecDevice.findAudioReceiverAddress());
        @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior =
                        getDeviceVolumeBehavior(getAvcAudioOutputDevice());

        // Condition 2: Already using full or absolute volume behavior
        boolean alreadyUsingFullOrAbsoluteVolume =
                currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL
                        || currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
        // Condition 3: CEC volume is enabled
        boolean cecVolumeEnabled =
                getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED;

        if (!cecVolumeEnabled || !alreadyUsingFullOrAbsoluteVolume) {
            disableAbsoluteVolumeControl();
            return;
        }

        // Check for safety: if the System Audio device is a candidate for AVC, we should already
        // have received messages from it to trigger the other conditions.
        if (systemAudioDeviceInfo == null) {
            disableAbsoluteVolumeControl();
            return;
        }
        // Condition 4: The System Audio device supports AVC (i.e. <Set Audio Volume Level>).
        switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) {
            case DeviceFeatures.FEATURE_SUPPORTED:
                if (!isAbsoluteVolumeControlEnabled()) {
                    // Start an action that will call {@link #enableAbsoluteVolumeControl}
                    // once the System Audio device sends <Report Audio Status>
                    localCecDevice.addAvcAudioStatusAction(
                            systemAudioDeviceInfo.getLogicalAddress());
                }
                return;
            case DeviceFeatures.FEATURE_NOT_SUPPORTED:
                disableAbsoluteVolumeControl();
                return;
            case DeviceFeatures.FEATURE_SUPPORT_UNKNOWN:
                disableAbsoluteVolumeControl();
                localCecDevice.queryAvcSupport(systemAudioDeviceInfo.getLogicalAddress());
                return;
            default:
                return;
        }
    }

AudioDeviceAttributes

比如??? AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_UNKNOWN flags=0x800 tags= bundle=null forVolume: true stream: STREAM_MUSIC(3)
?? ??? ?AudioDeviceAttributes: role:output type:hdmi addr: name: profiles:[] descriptors:[]

    @VisibleForTesting
    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI = new AudioDeviceAttributes(
            AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI, "");
    @VisibleForTesting
    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_ARC =
            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
                    AudioDeviceInfo.TYPE_HDMI_ARC, "");

disableAbsoluteVolumeControl 上面的4个条件不满足的时候,会取消设置avc,如果做过avc的设置,即将系统的avc设备的behaviour设置过DEVICE_VOLUME_BEHAVIOR_ABSOLUTE,那么会将behaviour重置为DEVICE_VOLUME_BEHAVIOR_FULL。对于盒子来说,这会让它始终通过CEC把音量键发给TV处理。

    private void disableAbsoluteVolumeControl() {
        if (isPlaybackDevice()) {
            playback().removeAvcAudioStatusAction();
        } else if (isTvDevice()) {
            tv().removeAvcAudioStatusAction();
        }
        AudioDeviceAttributes device = getAvcAudioOutputDevice();
        if (getDeviceVolumeBehavior(device) == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
            getAudioManager().setDeviceVolumeBehavior(device,
                    AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
        }
    }

一开始,Network里面记录的外部设备的avc状态必然是DeviceFeatures.FEATURE_SUPPORT_UNKNOWN, 则会启动SetAudioVolumeLevelDiscoveryAction来确认该设备是否支持。

    /**
     * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things
     * in parallel: send <Give Features> (to get <Report Features> in response),
     * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
     */
    @ServiceThreadOnly
    void queryAvcSupport(int targetAddress) {
        assertRunOnServiceThread();

        // Send <Give Features> if using CEC 2.0 or above.
        if (mService.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0) {
            synchronized (mLock) {
                mService.sendCecCommand(HdmiCecMessageBuilder.buildGiveFeatures(
                        getDeviceInfo().getLogicalAddress(), targetAddress));
            }
        }

        // If we don't already have a {@link SetAudioVolumeLevelDiscoveryAction} for the target
        // device, start one.
        List<SetAudioVolumeLevelDiscoveryAction> savlDiscoveryActions =
                getActions(SetAudioVolumeLevelDiscoveryAction.class);
        if (savlDiscoveryActions.stream().noneMatch(a -> a.getTargetAddress() == targetAddress)) {
            addAndStartAction(new SetAudioVolumeLevelDiscoveryAction(this, targetAddress,
                    new IHdmiControlCallback.Stub() {
                            @Override
                            public void onComplete(int result) {
                                if (result == HdmiControlManager.RESULT_SUCCESS) {
                                    getService().checkAndUpdateAbsoluteVolumeControlState();
                                }
                            }
                        }));
        }
    }
    boolean start() {
        sendCommand(SetAudioVolumeLevelMessage.build(
                getSourceAddress(), mTargetAddress, Constants.AUDIO_VOLUME_STATUS_UNKNOWN),
                result -> {
                    if (result == SendMessageResult.SUCCESS) {
                        // Message sent successfully; wait for <Feature Abort> in response
                        mState = STATE_WAITING_FOR_FEATURE_ABORT;
                        addTimer(mState, HdmiConfig.TIMEOUT_MS);
                    } else {
                        finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
                    }
                });
        return true;
    }

只有在target超时不回复时才算支持,否则就是不支持。

其中关于feature abort消息的处理,也不是直接更新状态的,而是在HdmiCecNetwork中有单独的处理。比如

??? CEC: logical_address: 0x04 device_type: 4 cec_version: 5 vendor_id: 1877008 display_name: GoogleTV8014 power_status: 0 physical_address: 0x3000 port_id: 0
????? Device features: record_tv_screen: N set_osd_string: N deck_control: N set_audio_rate: N arc_tx: N arc_rx: N set_audio_volume_level: N

??? [S] time=2022-08-15 19:09:56 message=<Report Physical Address> 4F:84:30:00:04
??? [S] time=2022-08-15 19:09:56 message=<Device Vendor Id> 4F:87:1C:A4:10
??? [S] time=2022-08-15 19:09:56 message=<Set Osd Name> 40:47 <Redacted len=12>
??? [S] time=2022-08-15 19:09:56 message=<Give System Audio Mode Status> 45:7D
??? [R] time=2022-08-15 19:09:57 message=<Give Osd Name> 04:46
??? [S] time=2022-08-15 19:09:57 message=<Set Audio Volume Level> 40:73:7F
??? [S] time=2022-08-15 19:09:57 message=<Set Osd Name> 40:47 <Redacted len=12>
??? [R] time=2022-08-15 19:09:57 message=<Give Device Vendor Id> 04:8C
??? [S] time=2022-08-15 19:09:57 message=<Device Vendor Id> 4F:87:1C:A4:10
??? [R] time=2022-08-15 19:09:57 message=<Feature Abort> 04:00:73:00

    /**
     * Updates the System Audio device's support for <Set Audio Volume Level> in the
     * {@link HdmiCecNetwork}. Can fail if the System Audio device is not in our
     * {@link HdmiCecNetwork}.
     *
     * @return Whether support was successfully updated in the network.
     */
    private boolean updateAvcSupport(
            @DeviceFeatures.FeatureSupportStatus int setAudioVolumeLevelSupport) {
        HdmiCecNetwork network = localDevice().mService.getHdmiCecNetwork();
        HdmiDeviceInfo currentDeviceInfo = network.getCecDeviceInfo(mTargetAddress);

        if (currentDeviceInfo == null) {
            return false;
        } else {
            network.updateCecDevice(
                    currentDeviceInfo.toBuilder()
                            .setDeviceFeatures(currentDeviceInfo.getDeviceFeatures().toBuilder()
                                    .setSetAudioVolumeLevelSupport(setAudioVolumeLevelSupport)
                                    .build())
                            .build()
            );
            return true;
        }
    }
    private void handleFeatureAbort(HdmiCecMessage message) {
        assertRunOnServiceThread();

        if (message.getParams().length < 2) {
            return;
        }

        int originalOpcode = message.getParams()[0] & 0xFF;
        int reason = message.getParams()[1] & 0xFF;

         // Check if we received <Feature Abort> in response to <Set Audio Volume Level>.
         // This provides information on whether the source supports the message.
        if (originalOpcode == Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL) {

            @DeviceFeatures.FeatureSupportStatus int featureSupport =
                    reason == Constants.ABORT_UNRECOGNIZED_OPCODE
                            ? DeviceFeatures.FEATURE_NOT_SUPPORTED
                            : DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;

            HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource());
            HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder()
                    .updateDeviceFeatures(
                            currentDeviceInfo.getDeviceFeatures().toBuilder()
                                    .setSetAudioVolumeLevelSupport(featureSupport)
                                    .build()
                    )
                    .build();
            updateCecDevice(newDeviceInfo);

            mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
        }
    }

当完成查询之后,又会再调用一次checkAndUpdateAbsoluteVolumeControlState来更新当前avc设备的状态。这个方法除了volume behaviour变更会执行之外,在在network进行新的设备addCecDevice和removeCecDevice会执行,,volume control开关变更会执行,system audio control变更也会执行。。。。

当连接的音频设备确认支持AVC以后,则会启动AbsoluteVolumeAudioStatusAction来完成从音频source一侧到reception的音量更新。这里涉及的消息和以前一样,是<Give Audio Status>和<Report Audio Status>。
?

    void addAvcAudioStatusAction(int targetAddress) {
        if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) {
            addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
        }
    }
    private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
        if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) {
            return false;
        }

        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
        int volume = HdmiUtils.getAudioStatusVolume(cmd);
        AudioStatus audioStatus = new AudioStatus(volume, mute);
        if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) {
            localDevice().getService().enableAbsoluteVolumeControl(audioStatus);
            mState = STATE_MONITOR_AUDIO_STATUS;
        } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
            if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
                localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume());
            }
            if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
                localDevice().getService().notifyAvcMuteChange(audioStatus.getMute());
            }
        }
        mLastAudioStatus = audioStatus;

        return true;
    }

enableAbsoluteVolumeControl,主要是设置AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener到AudioDeviceVolumeManager。再将其封装到DeviceVolumeDispatcherStub里面调用AudioService::registerDeviceVolumeDispatcherForAbsoluteVolume注册到AudioService,主要是将device配置为ABSOLUTE behaviour,并添加监听器。

    /**
     * Enables Absolute Volume Control. Should only be called when all the conditions for
     * AVC are met (see {@link #checkAndUpdateAbsoluteVolumeControlState}).
     * @param audioStatus The initial audio status to set the audio output device to
     */
    void enableAbsoluteVolumeControl(AudioStatus audioStatus) {
        HdmiCecLocalDevice localDevice = isPlaybackDevice() ? playback() : tv();
        HdmiDeviceInfo systemAudioDevice = getHdmiCecNetwork().getDeviceInfo(
                localDevice.findAudioReceiverAddress());
        VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
                .setMuted(audioStatus.getMute())
                .setVolumeIndex(audioStatus.getVolume())
                .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
                .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
                .build();
        mAbsoluteVolumeChangedListener = new AbsoluteVolumeChangedListener(
                localDevice, systemAudioDevice);

        // AudioService sets the volume of the stream and device based on the input VolumeInfo
        // when enabling absolute volume behavior, but not the mute state
        notifyAvcMuteChange(audioStatus.getMute());
        getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
                getAvcAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
                mAbsoluteVolumeChangedListener, true);
    }

AudioDeviceVolumeManager.java

    /**
     * @hide
     * Configures a device to use absolute volume model applied to different volume types, and
     * registers a listener for receiving volume updates to apply on that device
     * @param device the audio device set to absolute multi-volume mode
     * @param volumes the list of volumes the given device responds to
     * @param executor the Executor used for receiving volume updates through the listener
     * @param vclistener the callback for volume updates
     * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately
     *  from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume}
     *  will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}.
     */
    @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING,
            android.Manifest.permission.BLUETOOTH_PRIVILEGED })
    public void setDeviceAbsoluteMultiVolumeBehavior(
            @NonNull AudioDeviceAttributes device,
            @NonNull List<VolumeInfo> volumes,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
            boolean handlesVolumeAdjustment) {
        Objects.requireNonNull(device);
        Objects.requireNonNull(volumes);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(vclistener);

        final ListenerInfo listenerInfo = new ListenerInfo(
                vclistener, executor, device, handlesVolumeAdjustment);
        synchronized (mDeviceVolumeListenerLock) {
            if (mDeviceVolumeListeners == null) {
                mDeviceVolumeListeners = new ArrayList<>();
            }
            if (mDeviceVolumeListeners.size() == 0) {
                if (mDeviceVolumeDispatcherStub == null) {
                    mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub();
                }
            } else {
                mDeviceVolumeListeners.removeIf(info -> info.mDevice.equalTypeAddress(device));
            }
            mDeviceVolumeListeners.add(listenerInfo);
            mDeviceVolumeDispatcherStub.register(true, device, volumes, handlesVolumeAdjustment);
        }
    }

DeviceVolumeDispatcherStub继承IAudioDeviceVolumeDispatcher.Stub,是aidl实现跨进程进行AudioService音量变更监听的监听器。cec在统满足avc的条件时会通过register方法,通过AudioService的 registerDeviceVolumeDispatcherForAbsoluteVolume来实现最终的监听器设置。

其他的dispatchDeviceVolumeChanged和dispatchDeviceVolumeAdjusted 回调方法实现AudioService在音量调节时针对avc设备实现对cec service的通知。

    final class DeviceVolumeDispatcherStub extends IAudioDeviceVolumeDispatcher.Stub {
        /**
         * Register / unregister the stub
         * @param register true for registering, false for unregistering
         * @param device device for which volume is monitored
         */
        public void register(boolean register, @NonNull AudioDeviceAttributes device,
                @NonNull List<VolumeInfo> volumes, boolean handlesVolumeAdjustment) {
            try {
                getService().registerDeviceVolumeDispatcherForAbsoluteVolume(register,
                        this, mPackageName,
                        Objects.requireNonNull(device), Objects.requireNonNull(volumes),
                        handlesVolumeAdjustment);
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        }

        @Override
        public void dispatchDeviceVolumeChanged(
                @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol) {
            final ArrayList<ListenerInfo> volumeListeners;
            synchronized (mDeviceVolumeListenerLock) {
                volumeListeners = (ArrayList<ListenerInfo>) mDeviceVolumeListeners.clone();
            }
            for (ListenerInfo listenerInfo : volumeListeners) {
                if (listenerInfo.mDevice.equalTypeAddress(device)) {
                    listenerInfo.mExecutor.execute(
                            () -> listenerInfo.mListener.onAudioDeviceVolumeChanged(device, vol));
                }
            }
        }

        @Override
        public void dispatchDeviceVolumeAdjusted(
                @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol, int direction,
                int mode) {
            final ArrayList<ListenerInfo> volumeListeners;
            synchronized (mDeviceVolumeListenerLock) {
                volumeListeners = (ArrayList<ListenerInfo>) mDeviceVolumeListeners.clone();
            }
            for (ListenerInfo listenerInfo : volumeListeners) {
                if (listenerInfo.mDevice.equalTypeAddress(device)) {
                    listenerInfo.mExecutor.execute(
                            () -> listenerInfo.mListener.onAudioDeviceVolumeAdjusted(device, vol,
                                    direction, mode));
                }
            }
        }
    }

AudioService.java

在注册avc监听器的时候,主要是是做了4件事。

①更新avc device的volume behaviour,从full,fix列表中移除,在abs列表中添加。

②addAudioSystemDeviceOutToAbsVolumeDevices在添加列表之余,将AbsoluteVolumeDeviceInfo中的callback DeviceVolumeDispatcherStub也保存下来。

③通知volume behaviour变更,dispatchDeviceVolumeBehavior-->HdimControlService::onDeviceVolumeBehaviorChanged。也就是说在这个监听器注册的时候,就已经会被回调一次了。

④更新由cec 拿到的外部音频设备的音量。

此时在HdimControlService的记录就应该是这样的:

mIsAbsoluteVolumeControlEnabled: true

? mDeviceInfos:
??? CEC: logical_address: 0x00 device_type: 0 cec_version: 5 vendor_id: 1877008 display_name: xx power_status: 0 physical_address: 0x0000 port_id: -1
????? Device features: record_tv_screen: ? set_osd_string: ? deck_control: ? set_audio_rate: ? arc_tx: ? arc_rx: ? set_audio_volume_level: Y

AudioService

? mUseFixedVolume=false
? mFixedVolumeDevices=0x800,0x200000
? mFullVolumeDevices=0x40000,0x40001
? mAbsoluteVolumeDevices.keySet()=0x400

    @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING,
            android.Manifest.permission.BLUETOOTH_PRIVILEGED })
    public void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register,
            IAudioDeviceVolumeDispatcher cb, String packageName,
            AudioDeviceAttributes device, List<VolumeInfo> volumes,
            boolean handlesVolumeAdjustment) {
        // verify permissions
        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
                != PackageManager.PERMISSION_GRANTED
                && mContext.checkCallingOrSelfPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException(
                    "Missing MODIFY_AUDIO_ROUTING or BLUETOOTH_PRIVILEGED permissions");
        }
        // verify arguments
        Objects.requireNonNull(device);
        Objects.requireNonNull(volumes);

        int deviceOut = device.getInternalType();
        if (register) {
            AbsoluteVolumeDeviceInfo info = new AbsoluteVolumeDeviceInfo(
                    device, volumes, cb, handlesVolumeAdjustment);
            boolean volumeBehaviorChanged =
                    removeAudioSystemDeviceOutFromFullVolumeDevices(deviceOut)
                    | removeAudioSystemDeviceOutFromFixedVolumeDevices(deviceOut)
                    | (addAudioSystemDeviceOutToAbsVolumeDevices(deviceOut, info) == null);
            if (volumeBehaviorChanged) {
                dispatchDeviceVolumeBehavior(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
            }
            // Update stream volumes to the given device, if specified in a VolumeInfo.
            // Mute state is not updated because it is stream-wide - the only way to mute a
            // stream's output to a particular device is to set the volume index to zero.
            for (VolumeInfo volumeInfo : volumes) {
                if (volumeInfo.getVolumeIndex() != VolumeInfo.INDEX_NOT_SET
                        && volumeInfo.getMinVolumeIndex() != VolumeInfo.INDEX_NOT_SET
                        && volumeInfo.getMaxVolumeIndex() != VolumeInfo.INDEX_NOT_SET) {
                    if (volumeInfo.hasStreamType()) {
                        setStreamVolumeInt(volumeInfo.getStreamType(),
                                rescaleIndex(volumeInfo, volumeInfo.getStreamType()),
                                deviceOut, false /*force*/, packageName,
                                true /*hasModifyAudioSettings*/);
                    } else {
                        for (int streamType : volumeInfo.getVolumeGroup().getLegacyStreamTypes()) {
                            setStreamVolumeInt(streamType, rescaleIndex(volumeInfo, streamType),
                                    deviceOut, false /*force*/, packageName,
                                    true /*hasModifyAudioSettings*/);
                        }
                    }
                }
            }
        } else {
            boolean wasAbsVol = removeAudioSystemDeviceOutFromAbsVolumeDevices(deviceOut) != null;
            if (wasAbsVol) {
                dispatchDeviceVolumeBehavior(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE);
            }
        }
    }

3.avc volume change

AudioService.java

adjust

adjustVolumeStream() {
...

    if (isAbsoluteVolumeDevice(device)
                    && (flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0) {
                AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);
                dispatchAbsoluteVolumeChanged(streamType, info, newIndex);
            }
...
}

    private void dispatchAbsoluteVolumeChanged(int streamType, AbsoluteVolumeDeviceInfo deviceInfo,
            int index) {
        VolumeInfo volumeInfo = deviceInfo.getMatchingVolumeInfoForStream(streamType);
        if (volumeInfo != null) {
            try {
                deviceInfo.mCallback.dispatchDeviceVolumeChanged(deviceInfo.mDevice,
                        new VolumeInfo.Builder(volumeInfo)
                                .setVolumeIndex(rescaleIndex(index, streamType, volumeInfo))
                                .build());
            } catch (RemoteException e) {
                Log.w(TAG, "Couldn't dispatch absolute volume behavior volume change");
            }
        }
    }

AudioServiceVolumeManager.java

        @Override
        public void dispatchDeviceVolumeAdjusted(
                @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol, int direction,
                int mode) {
            final ArrayList<ListenerInfo> volumeListeners;
            synchronized (mDeviceVolumeListenerLock) {
                volumeListeners = (ArrayList<ListenerInfo>) mDeviceVolumeListeners.clone();
            }
            for (ListenerInfo listenerInfo : volumeListeners) {
                if (listenerInfo.mDevice.equalTypeAddress(device)) {
                    listenerInfo.mExecutor.execute(
                            () -> listenerInfo.mListener.onAudioDeviceVolumeAdjusted(device, vol,
                                    direction, mode));
                }
            }
        }

?adjust到最后也是发音量键,这个设计并不好,也重复了?

        /**
         * Called when AudioService adjusts the volume or mute state of an absolute volume
         * audio output device
         */
        @Override
        public void onAudioDeviceVolumeAdjusted(
                @NonNull AudioDeviceAttributes audioDevice,
                @NonNull VolumeInfo volumeInfo,
                @AudioManager.VolumeAdjustment int direction,
                @AudioDeviceVolumeManager.VolumeAdjustmentMode int mode
        ) {
            int keyCode;
            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:
                case AudioManager.ADJUST_MUTE:
                case AudioManager.ADJUST_UNMUTE:
                    // Many CEC devices only support toggle mute. Therefore, we send the
                    // same keycode for all three mute options.
                    keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
                    break;
                default:
                    return;
            }
            switch (mode) {
                case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL:
                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
                    break;
                case AudioDeviceVolumeManager.ADJUST_MODE_START:
                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
                    break;
                case AudioDeviceVolumeManager.ADJUST_MODE_END:
                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
                    break;
                default:
                    return;
            }
        }
    }

set

AudioService.java

setStreamVolume

 if (isAbsoluteVolumeDevice(device)
                    && ((flags & AudioManager.FLAG_ABSOLUTE_VOLUME) == 0)) {
                AbsoluteVolumeDeviceInfo info = mAbsoluteVolumeDeviceInfoMap.get(device);

                dispatchAbsoluteVolumeChanged(streamType, info, index);
            }

HdmiControlService.java

对于android device而言,由用户针对设备发起的音量调节,有遥控器/语音助手,其中遥控器更常规。如果连接的avr/tv支持avc,发送一个set device volume level message,比连续发几个十几个甚至几十个按键消息有效准确得多。

        /**
         * Called when AudioService sets the volume level of an absolute volume audio output device
         * to a numeric value.
         */
        @Override
        public void onAudioDeviceVolumeChanged(
                @NonNull AudioDeviceAttributes audioDevice,
                @NonNull VolumeInfo volumeInfo) {
            int localDeviceAddress;
            synchronized (mLocalDevice.mLock) {
                localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
            }
            sendCecCommand(SetAudioVolumeLevelMessage.build(
                            localDeviceAddress,
                            mSystemAudioDevice.getLogicalAddress(),
                            volumeInfo.getVolumeIndex()),
                    // If sending the message fails, ask the System Audio device for its
                    // audio status so that we can update AudioService
                    (int errorCode) -> {
                        if (errorCode == SendMessageResult.SUCCESS) {
                            // Update the volume tracked in our AbsoluteVolumeAudioStatusAction
                            // so it correctly processes incoming <Report Audio Status> messages
                            HdmiCecLocalDevice avcDevice = isTvDevice() ? tv() : playback();
                            avcDevice.updateAvcVolume(volumeInfo.getVolumeIndex());
                        } else {
                            sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
                                    localDeviceAddress,
                                    mSystemAudioDevice.getLogicalAddress()
                            ));
                        }
                    });
        }

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-24 21:08:24  更:2022-09-24 21:11:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 18:38:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码