【Android】(十) TetheringService

1 TetheringService 注册

//frameworks/base/services/java/com/android/server/SystemServer.java
t.traceBegin("StartTethering");
try {
    // TODO: hide implementation details, b/146312721.
    ConnectivityModuleConnector.getInstance().startModuleService(
            TETHERING_CONNECTOR_CLASS,
            PERMISSION_MAINLINE_NETWORK_STACK, service -> {
                ServiceManager.addService(Context.TETHERING_SERVICE, service,
                        false /* allowIsolated */,
                        DUMP_FLAG_PRIORITY_HIGH | DUMP_FLAG_PRIORITY_NORMAL);
            });
} catch (Throwable e) {
    reportWtf("starting Tethering", e);
}
t.traceEnd();

//packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/TetheringService.java
public void onCreate() {
    final TetheringDependencies deps = makeTetheringDependencies();
    // The Tethering object needs a fully functional context to start, so this can't be done
    // in the constructor.
    mConnector = new TetheringConnector(makeTethering(deps), TetheringService.this);

    mSettingsShim = SettingsShimImpl.newInstance();
}

/**
 * Make a reference to Tethering object.
 */
@VisibleForTesting
public Tethering makeTethering(TetheringDependencies deps) {
    return new Tethering(deps);
}

在 SystemServer 中启动了 TetheringService 服务,TetheringService 中创建了服务对应的 Binder 对象,并创建了 Tethering 对象。

2 开启热点

//packages/apps/Settings/src/com/android/settings/TetherEnabler.java
public void startTethering(int choice) {
    //...
    mConnectivityManager.startTethering(choice, true /* showProvisioningUi */,
            mOnStartTetheringCallback, mMainThreadHandler);
}

//packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java
public void startTethering(int type, boolean showProvisioningUi,
        final OnStartTetheringCallback callback, Handler handler) {
    //...
    getTetheringManager().startTethering(request, executor, tetheringCallback);
}

//packages/modules/Connectivity/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
public void startTethering(@NonNull final TetheringRequest request,
        @NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
    //...
    getConnector(c -> c.startTethering(request.getParcel(), callerPkg,
            getAttributionTag(), listener));
}

//packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/TetheringService.java
public void startTethering(TetheringRequestParcel request, String callerPkg,
        String callingAttributionTag, IIntResultListener listener) {
    //...
    mTethering.startTethering(request, listener);
}

//packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/Tethering.java
void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
    mHandler.post(() -> {
        //...
        enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
    });
}

从点击事件开始,经过一系列调用,最终调用到 Tethering 的 startTethering 函数中,向消息队列中添加一个 Executor,其中执行了 enableTetheringInternal。

//packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/Tethering.java
private void enableTetheringInternal(int type, boolean enable,
        final IIntResultListener listener) {
    int result = TETHER_ERROR_NO_ERROR;
    switch (type) {
        case TETHERING_WIFI:
            result = setWifiTethering(enable);
            break;
        //...
        case TETHERING_ETHERNET:
            result = setEthernetTethering(enable);
            break;
        //...
    }
    // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
    if (type != TETHERING_BLUETOOTH) {
        sendTetherResult(listener, result, type);
    }
}

private int setWifiTethering(final boolean enable) {
    try {
        final WifiManager mgr = getWifiManager();
        //...
        if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
                || (!enable && mgr.stopSoftAp())) {
            mWifiTetherRequested = enable;
            return TETHER_ERROR_NO_ERROR;
        }
    //...
}

根据传入的参数是 TETHERING_WIFI 还是 TETHERING_ETHERNET来确定开启哪种共享。接下来主要分析 WIFI 的情况。获取 WifiManager,来开启或者关闭 Wifi 热点。

//packages/modules/Wifi/framework/java/android/net/wifi/WifiManager.java
public boolean startTetheredHotspot(@Nullable SoftApConfiguration softApConfig) {
    try {
        return mService.startTetheredHotspot(softApConfig, mContext.getOpPackageName());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiServiceImpl.java
public boolean startTetheredHotspot(@Nullable SoftApConfiguration softApConfig,
        @NonNull String packageName) {
    //...
    WorkSource requestorWs = new WorkSource(Binder.getCallingUid(), packageName);
    //...
    if (!startSoftApInternal(new SoftApModeConfiguration(
            WifiManager.IFACE_IP_MODE_TETHERED, softApConfig,
            mTetheredSoftApTracker.getSoftApCapability(), LOCAL_ONLY_HOTSPOT_TYPE_NONE), requestorWs)) {
        mTetheredSoftApTracker.setFailedWhileEnabling();
        return false;
    }
    //...
    return true;
}

调用 WifiServiceImpl 的 startTetheredHotspot 方法,其中设置了 TetheredSoftApState,判断了是否能够创建更多的 SoftApManagers,接下来就是正式启动 SoftAp。

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiServiceImpl.java
private boolean startSoftApInternal(SoftApModeConfiguration apConfig, WorkSource requestorWs) {
    //...
    SoftApConfiguration softApConfig = apConfig.getSoftApConfiguration();
    //...
    mActiveModeWarden.startSoftAp(apConfig, requestorWs);
    return true;
}

//packages/modules/Wifi/service/java/com/android/server/wifi/ActiveModeWarden.java
public void startSoftAp(SoftApModeConfiguration softApConfig, WorkSource requestorWs) {
    mWifiController.sendMessage(WifiController.CMD_SET_AP, 1, 0,
            Pair.create(softApConfig, requestorWs));
}

case CMD_SET_AP:
    if (msg.arg1 == 1) {
        startSoftApModeManager(
                softApConfigAndWs.first, softApConfigAndWs.second);
    //...
    }

startSoftAp 向 WifiController 状态机发送了一个 CMD_SET_AP 消息,状态机处理该信息。

//packages/modules/Wifi/service/java/com/android/server/wifi/ActiveModeWarden.java
private void startSoftApModeManager(
        @NonNull SoftApModeConfiguration softApConfig, @NonNull WorkSource requestorWs) {
    //...
    WifiServiceImpl.SoftApCallbackInternal callback =
            softApConfig.getTargetMode() == IFACE_IP_MODE_LOCAL_ONLY
                    ? (softApConfig.getLohsType() == LOCAL_ONLY_HOTSPOT_TYPE_SECONDARY
                    ? mLohsCallbackSecondary : mLohsCallback) : mSoftApCallback;
    SoftApManager manager = mWifiInjector.makeSoftApManager(
            new SoftApListener(), callback, softApConfig, requestorWs,
            getRoleForSoftApIpMode(softApConfig.getTargetMode()), mVerboseLoggingEnabled);
    mSoftApManagers.add(manager);
}

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiInjector.java
public SoftApManager makeSoftApManager(
        @NonNull ActiveModeManager.Listener<SoftApManager> listener,
        @NonNull WifiServiceImpl.SoftApCallbackInternal callback,
        @NonNull SoftApModeConfiguration config,
        @NonNull WorkSource requestorWs,
        @NonNull ActiveModeManager.SoftApRole role,
        boolean verboseLoggingEnabled) {
    return new SoftApManager(mContext, mWifiHandlerThread.getLooper(),
            mFrameworkFacade, mWifiNative, mCoexManager, mCountryCode.getCountryCode(),
            listener, callback, mWifiApConfigStore, config, mWifiMetrics, mSarManager,
            mWifiDiagnostics, new SoftApNotifier(mContext, mFrameworkFacade,
            mWifiNotificationManager), mCmiMonitor, mActiveModeWarden,
            mClock.getElapsedSinceBootMillis(), requestorWs, role, verboseLoggingEnabled);
}

//packages/modules/Wifi/service/java/com/android/server/wifi/SoftApManager.java
public SoftApManager(
        @NonNull WifiContext context,
        @NonNull Looper looper,
        @NonNull FrameworkFacade framework,
        @NonNull WifiNative wifiNative,
        @NonNull CoexManager coexManager,
        String countryCode,
        @NonNull Listener<SoftApManager> listener,
        @NonNull WifiServiceImpl.SoftApCallbackInternal callback,
        @NonNull WifiApConfigStore wifiApConfigStore,
        @NonNull SoftApModeConfiguration apConfig,
        @NonNull WifiMetrics wifiMetrics,
        @NonNull SarManager sarManager,
        @NonNull WifiDiagnostics wifiDiagnostics,
        @NonNull SoftApNotifier softApNotifier,
        @NonNull ClientModeImplMonitor cmiMonitor,
        @NonNull ActiveModeWarden activeModeWarden,
        long id,
        @NonNull WorkSource requestorWs,
        @NonNull SoftApRole role,
        boolean verboseLoggingEnabled) {
    //...
    mStateMachine = new SoftApStateMachine(looper);
    configureInternalConfiguration();
    //...
    mStateMachine.sendMessage(SoftApStateMachine.CMD_START, requestorWs);
}

WifiController 收到消息以后创建了 SoftApModeManager。在 SoftApModeManager 的构造函数中创建了其内部状态机,并向状态机发送了一个 CMD_START 消息。

//packages/modules/Wifi/service/java/com/android/server/wifi/SoftApManager.java
case CMD_START:
    mRequestorWs = (WorkSource) message.obj;
    if (mCurrentSoftApConfiguration == null
            || mCurrentSoftApConfiguration.getSsid() == null) {
        Log.e(getTag(), "Unable to start soft AP without valid configuration");
        updateApState(WifiManager.WIFI_AP_STATE_FAILED,
                WifiManager.WIFI_AP_STATE_DISABLED,
                WifiManager.SAP_START_FAILURE_GENERAL);
        mWifiMetrics.incrementSoftApStartResult(
                false, WifiManager.SAP_START_FAILURE_GENERAL);
        mModeListener.onStartFailure(SoftApManager.this);
        break;
    }
    if (isBridgedMode()) {
        boolean isFallbackToSingleAp = false;
        int newSingleApBand = 0;
        for (ClientModeManager cmm
                : mActiveModeWarden.getClientModeManagers()) {
            WifiInfo wifiConnectedInfo = cmm.syncRequestConnectionInfo();
            int wifiFrequency = wifiConnectedInfo.getFrequency();
            if (wifiFrequency > 0
                    && !mSafeChannelFrequencyList.contains(
                    wifiFrequency)) {
                Log.d(getTag(), "Wifi connected to unavailable freq: "
                        + wifiFrequency);
                isFallbackToSingleAp = true;
                break;
            }
        }
        for (int configuredBand : mCurrentSoftApConfiguration.getBands()) {
            int availableBand = ApConfigUtil.removeUnavailableBands(
                    mCurrentSoftApCapability,
                    configuredBand, mCoexManager);
            if (configuredBand != availableBand) {
                isFallbackToSingleAp = true;
            }
            newSingleApBand |= availableBand;
        }
        if (isFallbackToSingleAp) {
            newSingleApBand = ApConfigUtil.append24GToBandIf24GSupported(
                    newSingleApBand, mContext);
            Log.i(getTag(), "Fallback to single AP mode with band "
                    + newSingleApBand);
            mCurrentSoftApConfiguration =
                    new SoftApConfiguration.Builder(mCurrentSoftApConfiguration)
                    .setBand(newSingleApBand)
                    .build();
        }
    }
    setSoftApIfaceOnSecondWlan();
    Log.d(getTag(), "wifi.softap.iface.on.dual.wlan set to " + WifiProperties.softap_iface_on_dual_wlan());

    // Remove 6GHz from requested bands if security type is restricted
    // Note: 6GHz only band is already handled by initial validation
    mCurrentSoftApConfiguration =
            ApConfigUtil.remove6gBandForUnsupportedSecurity(
                mCurrentSoftApConfiguration);

    mApInterfaceName = mWifiNative.setupInterfaceForSoftApMode(
            mWifiNativeInterfaceCallback, mRequestorWs,
            mCurrentSoftApConfiguration.getBand(), isBridgedMode());
    if (TextUtils.isEmpty(mApInterfaceName)) {
        Log.e(getTag(), "setup failure when creating ap interface.");
        updateApState(WifiManager.WIFI_AP_STATE_FAILED,
                WifiManager.WIFI_AP_STATE_DISABLED,
                WifiManager.SAP_START_FAILURE_GENERAL);
        mWifiMetrics.incrementSoftApStartResult(
                false, WifiManager.SAP_START_FAILURE_GENERAL);
        mModeListener.onStartFailure(SoftApManager.this);
        break;
    }
    mSoftApNotifier.dismissSoftApShutdownTimeoutExpiredNotification();
    updateApState(WifiManager.WIFI_AP_STATE_ENABLING,
            WifiManager.WIFI_AP_STATE_DISABLED, 0);
    int result = startSoftAp();
    if (result != SUCCESS) {
        int failureReason = WifiManager.SAP_START_FAILURE_GENERAL;
        if (result == ERROR_NO_CHANNEL) {
            failureReason = WifiManager.SAP_START_FAILURE_NO_CHANNEL;
        } else if (result == ERROR_UNSUPPORTED_CONFIGURATION) {
            failureReason = WifiManager
                    .SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION;
        }
        updateApState(WifiManager.WIFI_AP_STATE_FAILED,
                WifiManager.WIFI_AP_STATE_ENABLING,
                failureReason);
        stopSoftAp();
        mWifiMetrics.incrementSoftApStartResult(false, failureReason);
        mModeListener.onStartFailure(SoftApManager.this);
        break;
    }
    transitionTo(mStartedState);
    break;

首先,代码获取传入的 WorkSource 对象,这个对象用于标识 Soft AP 的请求者。

然后,在进行一些检查。如果当前的 Soft AP 配置为 null 或者配置的 SSID 为 null,说明没有有效的配置,无法启动Soft AP。这时,代码会更新 Soft AP 的状态为失败,增加相关的统计数据,并通过回调通知监听器。

接下来判断是否启用了 bridged 模式。如果是,需要进行一些额外的处理。首先,代码会遍历当前处于连接状态的所有 ClientModeManager 实例,并检查其连接的 WiFi 频率是否可用。如果有连接到不可用频率的 WiFi,说明无法启用 bridged 模式,需要回退到单 AP 模式。然后,代码会遍历 Soft AP 配置中的频段参数,并通过ApConfigUtil.removeUnavailableBands 方法判断每个频段是否可用。如果有不可用的频段,也需要回退到单AP模式。如果回退到了单 AP 模式,代码会通过 ApConfigUtil.append24GToBandIf24GSupported 方法将2.4GHz频段添加到可用的频段中,并更新 Soft AP 配置。

接下来调用 WifiNative 的方法设置 Soft AP 的接口。如果设置失败,说明无法创建 AP 接口,代码会更新 Soft AP 的状态为失败,并终止启动流程。

接下来调用 startSoftAp 方法启动 Soft AP。如果启动失败,代码会根据不同的失败原因更新 Soft AP的状态,并调用stopSoftAp 方法停止 Soft AP,并进行相应的统计和回调处理。

//packages/modules/Wifi/service/java/com/android/server/wifi/SoftApManager.java
mApInterfaceName = mWifiNative.setupInterfaceForSoftApMode(
        mWifiNativeInterfaceCallback, mRequestorWs,
        mCurrentSoftApConfiguration.getBand(), isBridgedMode());
        
//packages/modules/Wifi/service/java/com/android/server/wifi/WifiNative.java
public String setupInterfaceForSoftApMode(
        @NonNull InterfaceCallback interfaceCallback, @NonNull WorkSource requestorWs,
        @SoftApConfiguration.BandType int band, boolean isBridged) {
    synchronized (mLock) {
        if (!startHal()) {
            Log.e(TAG, "Failed to start Hal");
            mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToHal();
            return null;
        }
        if (!startHostapd()) {
            Log.e(TAG, "Failed to start hostapd");
            mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToHostapd();
            return null;
        }
        Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_AP);
        if (iface == null) {
            Log.e(TAG, "Failed to allocate new AP iface");
            return null;
        }
        iface.externalListener = interfaceCallback;
        iface.name = createApIface(iface, requestorWs, band, isBridged);
        if (TextUtils.isEmpty(iface.name)) {
            Log.e(TAG, "Failed to create AP iface in vendor HAL");
            mIfaceMgr.removeIface(iface.id);
            mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToHal();
            return null;
        }
        String ifaceInstanceName = iface.name;
        if (isBridged) {
            List<String> instances = getBridgedApInstances(iface.name);
            if (instances == null || instances.size() == 0) {
                Log.e(TAG, "Failed to get bridged AP instances" + iface.name);
                teardownInterface(iface.name);
                mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToHal();
                return null;
            }
            // Always select first instance as wificond interface.
            ifaceInstanceName = instances.get(0);
        }
        if (!mWifiCondManager.setupInterfaceForSoftApMode(ifaceInstanceName)) {
            Log.e(TAG, "Failed to setup iface in wificond on " + iface);
            teardownInterface(iface.name);
            mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToWificond();
            return null;
        }
        iface.networkObserver = new NetworkObserverInternal(iface.id);
        if (!registerNetworkObserver(iface.networkObserver)) {
            Log.e(TAG, "Failed to register network observer on " + iface);
            teardownInterface(iface.name);
            return null;
        }
        // Just to avoid any race conditions with interface state change callbacks,
        // update the interface state before we exit.
        onInterfaceStateChanged(iface, isInterfaceUp(iface.name));
        Log.i(TAG, "Successfully setup " + iface);

        iface.featureSet = getSupportedFeatureSetInternal(iface.name);
        return iface.name;
    }
}

首先,代码启动并检查是否成功启动了 VendorHal 和 hostapdHal。如果启动失败,则增加失败计数并返回null。

然后,分配一个AP接口,如果分配失败,则返回null。

接着,将 interfaceCallback 设置为外部监听器,为 AP 接口创建一个名称,并将该名称赋值给iface.name。如果创建名称失败,则增加失败计数并返回null。

如果 isBridged 为 true,则调用 getBridgedApInstances 方法获取桥接的 AP 实例列表。如果获取失败或者列表为空,则返回 null。否则,默认选择第一个实例作为 wificond 接口,将其名称赋值给 ifaceInstanceName。

然后,调用 mWifiCondManager 的 setupInterfaceForSoftApMode 方法去设置 wificond 中的接口。如果设置失败,则增加失败计数并返回null。

接着,创建一个 NetworkObserverInternal 对象并将其赋值给 iface.networkObserver。然后,调用registerNetworkObserver方法去注册网络观察器。如果注册失败,则返回null。

最后,更新接口状态并返回接口名称。

//packages/modules/Wifi/service/java/com/android/server/wifi/SoftApManager.java
private int startSoftAp() {
    Log.d(getTag(), "band " + mCurrentSoftApConfiguration.getBand() + " iface "
            + mApInterfaceName + " country " + mCountryCode);

    int result = setMacAddress();
    if (result != SUCCESS) {
        return result;
    }

    result = setCountryCode();
    if (result != SUCCESS) {
        return result;
    }

    // Make a copy of configuration for updating AP band and channel.
    SoftApConfiguration.Builder localConfigBuilder =
            new SoftApConfiguration.Builder(mCurrentSoftApConfiguration);

    boolean acsEnabled = mCurrentSoftApCapability.areFeaturesSupported(
            SoftApCapability.SOFTAP_FEATURE_ACS_OFFLOAD);

    result = ApConfigUtil.updateApChannelConfig(
            mWifiNative, mCoexManager, mContext.getResources(), mCountryCode,
            localConfigBuilder, mCurrentSoftApConfiguration, acsEnabled);
    if (result != SUCCESS) {
        Log.e(getTag(), "Failed to update AP band and channel");
        return result;
    }

    if (mCurrentSoftApConfiguration.isHiddenSsid()) {
        Log.d(getTag(), "SoftAP is a hidden network");
    }

    if (!ApConfigUtil.checkSupportAllConfiguration(
            mCurrentSoftApConfiguration, mCurrentSoftApCapability)) {
        Log.d(getTag(), "Unsupported Configuration detect! config = "
                + mCurrentSoftApConfiguration);
        return ERROR_UNSUPPORTED_CONFIGURATION;
    }

    if (!mWifiNative.startSoftAp(mApInterfaceName,
              localConfigBuilder.build(),
              mOriginalModeConfiguration.getTargetMode() ==  WifiManager.IFACE_IP_MODE_TETHERED,
              mSoftApListener)) {
        Log.e(getTag(), "Soft AP start failed");
        return ERROR_GENERIC;
    }

调用 setMacAddress 函数来设置MAC地址。调用 setCountryCode 函数来设置国家码。

在进行频段和信道配置之前,首先创建一个当前配置的副本,以便在更新配置时不会修改原始配置。然后根据当前设备支持的功能,判断是否启用ACS(自动信道选择)功能。

调用 ApConfigUtil.updateApChannelConfig 函数来更新 SoftAP 的频段和信道配置。该函数会根据当前设备支持的功能和ACS的启用状态来设置频段和信道。

如果当前设备不支持 SoftAP 配置中的所有要求,则返回错误代码。

最后调用 mWifiNative.startSoftAp 函数来启动 SoftAP。如果启动失败,函数会返回错误代码。

//packages/modules/Wifi/service/java/com/android/server/wifi/WifiNative.java
public boolean startSoftAp(
        @NonNull String ifaceName, SoftApConfiguration config, boolean isMetered,
        SoftApListener listener) {
    if (mHostapdHal.isApInfoCallbackSupported()) {
        if (!mHostapdHal.registerApCallback(ifaceName, listener)) {
            Log.e(TAG, "Failed to register ap listener");
            return false;
        }
    } else {
        SoftApListenerFromWificond softApListenerFromWificond =
                new SoftApListenerFromWificond(ifaceName, listener);
        if (!mWifiCondManager.registerApCallback(ifaceName,
                Runnable::run, softApListenerFromWificond)) {
            Log.e(TAG, "Failed to register ap listener from wificond");
            return false;
        }
    }

    if (!mHostapdHal.addAccessPoint(ifaceName, config, isMetered, listener::onFailure)) {
        Log.e(TAG, "Failed to add acccess point");
        mWifiMetrics.incrementNumSetupSoftApInterfaceFailureDueToHostapd();
        return false;
    }

    return true;
}

调用 addAccessPoint,接下来调用到 HAL 层中,HAL 层暂不分析。

//packages/modules/Wifi/service/java/com/android/server/wifi/SoftApManager.java
private void onUpChanged(boolean isUp) {

    if (isUp) {
        Log.d(getTag(), "SoftAp is ready for use");
        updateApState(WifiManager.WIFI_AP_STATE_ENABLED,
                WifiManager.WIFI_AP_STATE_ENABLING, 0);
        //...
        }
    } 
    //..
}

此处启动成功以后,SoftApManager 进入 startedState,在其中的 enter 方法中判断了接口是否启动,若启动则发送一个广播通报 SoftAp 的状态由 WIFI_AP_STATE_ENABLING 转变为 WIFI_AP_STATE_ENABLED。

Tethering 注册了这个广播接收器,收到以后依次调用 handleWifiApAction -> enableWifiIpServing -> enableIpServing -> changeInterfaceState -> tether,然后向 IpServer 发送一个 CMD_TETHER_REQUESTED 消息。收到这个消息以后,如 DHCP 启动流程分析的,IpServer 进入 TetheredState,在 enter 中向TetherMainStateMachine发送一个 EVENT_IFACE_SERVING_STATE_ACTIVE 消息让其进入 TetherModeAliveState 状态。在 TetherModeAliveState 的 enter 中,调用了 turnOnMainTetherSettings 启动了 dnsmasq。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值