android14网络共存,网络优先级

android14双网共存和网络优先级

system-netd-server-rooutecontroller.patch

diff --git a/server/RouteController.cpp b/server/RouteController.cpp
index 97ca84e9..91f19607 100644
--- a/server/RouteController.cpp
+++ b/server/RouteController.cpp
@@ -777,6 +777,18 @@ int RouteController::modifyVpnFallthroughRule(uint16_t action, unsigned vpnNetId
                         fwmark.intValue, mask.intValue);
 }
 
+
+// added by cgt ethernet and wifi coexist start
+// Add a new rule to look up the 'main' table, with the same selectors as the "default network"
+// rule, but with a lower priority. We will never create routes in the main table; it should only be
+// used for directly-connected routes implicitly created by the kernel when adding IP addresses.
+// This is necessary, for example, when adding a route through a directly-connected gateway: in
+// order to add the route, there must already be a directly-connected route that covers the gateway.
+[[nodiscard]] static int addDirectlyConnectedRule() {
+    return modifyIpRule(RTM_NEWRULE, RULE_PRIORITY_DIRECTLY_CONNECTED, RT_TABLE_MAIN,
+                        MARK_UNSET, MARK_UNSET, IIF_NONE, OIF_NONE, INVALID_UID, INVALID_UID);
+}
+
 /* static */
 int RouteController::configureDummyNetwork() {
     const char *interface = DummyNetwork::INTERFACE_NAME;
@@ -1305,6 +1317,9 @@ int RouteController::Init(unsigned localNetId) {
     if (int ret = addLocalNetworkRules(localNetId)) {
         return ret;
     }
+   if (int ret = addDirectlyConnectedRule()) {
+       return ret;
+    }    
     if (int ret = addUnreachableRule()) {
         return ret;
     }
diff --git a/server/RouteController.h b/server/RouteController.h
index a56d4e05..501a1b2b 100644
--- a/server/RouteController.h
+++ b/server/RouteController.h
@@ -43,6 +43,7 @@ constexpr int32_t RULE_PRIORITY_PROHIBIT_NON_VPN                  = 14000;
 // not have the necessary permission bits in the fwmark. We cannot just give any socket on any of
 // these networks the permission bits, because if the UID that created the socket loses access to
 // the network, then the socket must not match any rule that selects that network.
+constexpr int32_t RULE_PRIORITY_DIRECTLY_CONNECTED                = 9999;
 constexpr int32_t RULE_PRIORITY_UID_EXPLICIT_NETWORK              = 15000;
 constexpr int32_t RULE_PRIORITY_EXPLICIT_NETWORK                  = 16000;
 constexpr int32_t RULE_PRIORITY_OUTPUT_INTERFACE                  = 17000;

connectivity.path

diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 193bd920d2..7e4494a1c0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -73,12 +73,14 @@ package android.net {
 
   public class ConnectivitySettingsManager {
     method public static void clearGlobalProxy(@NonNull android.content.Context);
+    method public static boolean getBluetoothAlwaysRequested(@NonNull android.content.Context, boolean);
     method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
     method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
     method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
     method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context);
     method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
     method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int);
+    method public static boolean getEthernetAlwaysRequested(@NonNull android.content.Context, boolean);
     method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context);
     method public static long getIngressRateLimitInBytesPerSecond(@NonNull android.content.Context);
     method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -94,12 +96,14 @@ package android.net {
     method @NonNull public static java.util.Set<java.lang.Integer> getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
     method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
     method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setBluetoothAlwaysRequested(@NonNull android.content.Context, boolean);
     method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
     method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
     method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
     method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>);
     method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
     method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
+    method public static void setEthernetAlwaysRequested(@NonNull android.content.Context, boolean);
     method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
     method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=-1L, to=4294967295L) long);
     method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -115,9 +119,11 @@ package android.net {
     method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
     method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
     method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    field public static final String BLUETOOTH_ALWAYS_REQUESTED = "bluetooth_always_requested";
     field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
     field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0
     field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1
+    field public static final String ETHERNET_ALWAYS_REQUESTED = "ethernet_always_requested";
     field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2
     field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0
     field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 822e67d339..8ef43a9a18 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -291,6 +291,10 @@ public class ConnectivitySettingsManager {
      */
     public static final String WIFI_ALWAYS_REQUESTED = "wifi_always_requested";
 
+
+     public static final String ETHERNET_ALWAYS_REQUESTED = "ethernet_always_requested";
+     
+     public static final String BLUETOOTH_ALWAYS_REQUESTED = "bluetooth_always_requested";
     /**
      * Whether to automatically switch away from wifi networks that lose Internet access.
      * Only meaningful if config_networkAvoidBadWifi is set to 0, otherwise the system always
@@ -902,6 +906,64 @@ public class ConnectivitySettingsManager {
                 context.getContentResolver(), WIFI_ALWAYS_REQUESTED, (enable ? 1 : 0));
     }
 
+    /**
+     * Read from {@link Settings} whether the ethernet data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default value if no setting value.
+     * @return Whether the ethernet data connection should remain active even when higher
+     *         priority networks are active.
+     */
+    public static boolean getEthernetAlwaysRequested(@NonNull Context context, boolean def) {
+        final int enable = Settings.Global.getInt(
+                context.getContentResolver(), ETHERNET_ALWAYS_REQUESTED, (def ? 1 : 0));
+        return (enable != 0) ? true : false;
+    }
+
+    /**
+     * Write into {@link Settings} whether the ethernet data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param enable Whether the ethernet data connection should remain active even when higher
+     *               priority networks are active
+     */
+    public static void setEthernetAlwaysRequested(@NonNull Context context, boolean enable) {
+        Settings.Global.putInt(
+                context.getContentResolver(), ETHERNET_ALWAYS_REQUESTED, (enable ? 1 : 0));
+    }
+
+/**
+ * Read from {@link Settings} whether the bluetooth data connection should remain active
+ * even when higher priority networks are active.
+ *
+ * @param context The {@link Context} to query the setting.
+ * @param def The default value if no setting value.
+ * @return Whether the bluetooth data connection should remain active even when higher
+ *         priority networks are active.
+ */
+public static boolean getBluetoothAlwaysRequested(@NonNull Context context, boolean def) {
+    final int enable = Settings.Global.getInt(
+            context.getContentResolver(), BLUETOOTH_ALWAYS_REQUESTED, (def ? 1 : 0));
+    return (enable != 0) ? true : false;
+}
+
+/**
+ * Write into {@link Settings} whether the bluetooth data connection should remain active
+ * even when higher priority networks are active.
+ *
+ * @param context The {@link Context} to set the setting.
+ * @param enable Whether the bluetooth data connection should remain active even when higher
+ *               priority networks are active
+ */
+public static void setBluetoothAlwaysRequested(@NonNull Context context, boolean enable) {
+    Settings.Global.putInt(
+            context.getContentResolver(), BLUETOOTH_ALWAYS_REQUESTED, (enable ? 1 : 0));
+}
+
+
+
     /**
      * Get avoid bad wifi setting from {@link Settings}.
      *
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 59b50bb6f9..c9c458e0b8 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -742,6 +742,16 @@ public class EthernetNetworkFactory {
             if (mNetworkOfferCallback == null) {
                 mNetworkOfferCallback = new EthernetNetworkOfferCallback();
             }
+	
+	   if(name!=null && name.equals("eth1")){
+
+    		 //mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+     		 mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+     		 //mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+     		 Log.d(TAG, "hcq mCapabilities: " + mCapabilities);
+	     }
+
+
             mNetworkProvider.registerNetworkOffer(getNetworkScore(),
                     new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
                     mNetworkOfferCallback);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 120486c1df..4a86b2c040 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -357,8 +357,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
     private static final String TRAFFICCONTROLLER_ARG = "trafficcontroller";
 
     private static final boolean DBG = true;
-    private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final boolean DDBG = true; //Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VDBG = true; //Log.isLoggable(TAG, Log.VERBOSE);
 
     private static final boolean LOGD_BLOCKED_NETWORKINFO = true;
 
@@ -1608,6 +1608,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
         mDefaultVehicleRequest = createAlwaysOnRequestForCapability(
                 NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL,
                 NetworkRequest.Type.BACKGROUND_REQUEST);
+				
+		mDefaultEthernetRequest = createDefaultInternetRequestForTransport(
+                NetworkCapabilities.TRANSPORT_ETHERNET, NetworkRequest.Type.BACKGROUND_REQUEST);
+				
+		mDefaultBluetoothRequest = createDefaultInternetRequestForTransport(
+                NetworkCapabilities.TRANSPORT_BLUETOOTH, NetworkRequest.Type.BACKGROUND_REQUEST);
 
         mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
         // TODO: Consider making the timer customizable.
@@ -1928,10 +1934,16 @@ public class ConnectivityService extends IConnectivityManager.Stub
         handleAlwaysOnNetworkRequest(mDefaultMobileDataRequest,
                 ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, true /* defaultValue */);
         handleAlwaysOnNetworkRequest(mDefaultWifiRequest,
-                ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED, false /* defaultValue */);
+                ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED, true /* defaultValue */);
         final boolean vehicleAlwaysRequested = mResources.get().getBoolean(
                 R.bool.config_vehicleInternalNetworkAlwaysRequested);
         handleAlwaysOnNetworkRequest(mDefaultVehicleRequest, vehicleAlwaysRequested);
+		
+		handleAlwaysOnNetworkRequest(mDefaultEthernetRequest,
+                ConnectivitySettingsManager.ETHERNET_ALWAYS_REQUESTED, true /* defaultValue */);
+				
+	    handleAlwaysOnNetworkRequest(mDefaultEthernetRequest,
+                ConnectivitySettingsManager.BLUETOOTH_ALWAYS_REQUESTED, true /* defaultValue */);
     }
 
     // Note that registering observer for setting do not get initial callback when registering,
@@ -1962,6 +1974,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
                 Settings.Global.getUriFor(
                         ConnectivitySettingsManager.INGRESS_RATE_LIMIT_BYTES_PER_SECOND),
                 EVENT_INGRESS_RATE_LIMIT_CHANGED);
+				
+	    // Watch for whether to keep ethernet always on.
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(ConnectivitySettingsManager.ETHERNET_ALWAYS_REQUESTED),
+                EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
     }
 
     private void registerPrivateDnsSettingsCallbacks() {
@@ -4925,6 +4942,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
     private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
         ensureRunningOnConnectivityServiceThread();
 
+	boolean needed = nai.isWIFI();
+	if(needed)
+		return false;
+
         if (!nai.everConnected() || nai.isVPN() || nai.isInactive()
                 || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) {
             return false;
@@ -7751,6 +7772,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
     // Request used to optionally keep vehicle internal network always active
     private final NetworkRequest mDefaultVehicleRequest;
 
+    private final NetworkRequest mDefaultEthernetRequest;
+	
+	private final NetworkRequest mDefaultBluetoothRequest;
     // Sentinel NAI used to direct apps with default networks that should have no connectivity to a
     // network with no service. This NAI should never be matched against, nor should any public API
     // ever return the associated network. For this reason, this NAI is not in the list of available
@@ -9044,7 +9068,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                 break;
             }
         }
-        nai.disconnect();
+        // nai.disconnect();
     }
 
     private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 85282cbc9b..b6cc1b7b75 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1181,6 +1181,15 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
         return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
     }
 
+    /** Whether this network is a WIFI. */
+    public boolean isWIFI() {
+        return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
+    }
+
+    /** Whether this network is a ETHERNET. */
+    public boolean isETHERNET() {
+        return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
+    }    
     /**
      * Whether this network should propagate the capabilities from its underlying networks.
      * Currently only true for VPNs.
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index d94c8dceeb..d1f1a9082f 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -50,11 +50,17 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Predicate;
+import android.util.Log;
+import android.os.SystemProperties;
 
 /**
  * A class that knows how to find the best network matching a request out of a list of networks.
  */
 public class NetworkRanker {
+	private static final String TAG = "NetworkRanker";
+	private static final String PERSIST_NET_PREFERRED_TRANSPORTS_ORDER = "persist.net.preferred.transports.order";
+	
+	private static int[] preferredTransportsOrder;
     /**
      * Home for all configurations of NetworkRanker
      */
@@ -118,10 +124,63 @@ public class NetworkRanker {
         return getBestNetworkByPolicy(candidates, currentSatisfier);
     }
 
+    //public static final int TRANSPORT_BLUETOOTH = 2;
+    //public static final int TRANSPORT_CELLULAR = 0;
+    //public static final int TRANSPORT_ETHERNET = 3;
+    //public static final int TRANSPORT_WIFI = 1;
+	
+	
     // Transport preference order, if it comes down to that.
-    private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
+    private static int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
             TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };
 
+    private static int[] parseTransportsOrder(String transportsOrderStr) {
+        String[] transportStrings = transportsOrderStr.split(",");
+        int[] transportsOrder = new int[transportStrings.length];
+
+        for (int i = 0; i < transportStrings.length; i++) {
+            try {
+                int transport = Integer.parseInt(transportStrings[i].trim());
+                switch (transport) {
+                    case TRANSPORT_BLUETOOTH:
+                    case TRANSPORT_CELLULAR:
+                    case TRANSPORT_ETHERNET:
+                    case TRANSPORT_WIFI:
+                        transportsOrder[i] = transport;
+                        break;
+                    default:
+                        Log.w(TAG, "Invalid transport value: " + transport);
+                        return null;
+                }
+            } catch (NumberFormatException e) {
+                Log.e(TAG, "Invalid transport format: " + transportsOrderStr);
+                return null;
+            }
+        }
+
+        return transportsOrder;
+    }
+	
+	
+    static void getPreferredTransportsOrder() {
+        String transportsOrderStr = SystemProperties.get(PERSIST_NET_PREFERRED_TRANSPORTS_ORDER);
+        if (transportsOrderStr == null || transportsOrderStr.isEmpty()) {
+            Log.w(TAG, "No preferred transports order set, using default.");
+            return;
+        }
+
+        preferredTransportsOrder = parseTransportsOrder(transportsOrderStr);
+        if (preferredTransportsOrder == null) {
+            Log.e(TAG, "Failed to parse the transports order string.");
+            return;
+        }
+       
+        Log.i(TAG, "Preferred transports order set to: " + Arrays.toString(preferredTransportsOrder));
+		
+		
+    }
+	
+
     // Function used to partition a list into two working areas depending on whether they
     // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
     // items that don't will be put in |negative|.
@@ -324,6 +383,12 @@ public class NetworkRanker {
         // change from the previous result. If there were, it's guaranteed candidates.size() > 0
         // because accepted.size() > 0 above.
 
+        getPreferredTransportsOrder();
+		
+		if(preferredTransportsOrder != null)
+			PREFERRED_TRANSPORTS_ORDER = preferredTransportsOrder;
+		
+		Log.i(TAG, "Confirm candidates transports order" + Arrays.toString(PREFERRED_TRANSPORTS_ORDER));
         // If some of the networks have a better transport than others, keep only the ones with
         // the best transports.
         for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
@@ -381,6 +446,12 @@ public class NetworkRanker {
         // If there is no satisfying network, then this network can beat, because some network
         // is always better than no network.
         if (null == champion) return true;
+
+	boolean needed = contestant.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
+
+	if(needed)
+		return true;
+
         // If there is no champion, the offer can always beat.
         // Otherwise rank them.
         final ArrayList<Scoreable> candidates = new ArrayList<>();

使用
public static final int TRANSPORT_BLUETOOTH = 2;
public static final int TRANSPORT_CELLULAR = 0;
public static final int TRANSPORT_ETHERNET = 3;
public static final int TRANSPORT_WIFI = 1;

1、//以太网优先 > WIFI> 4G>蓝牙
setprop persist.net.preferred.transports.order “3,1,0,2”

2、//4G优先>WIFI>以太网>BLUETOOTH
setprop persist.net.preferred.transports.order “0,1,3,2”

3、// wifi优先 >4G>以太网>蓝牙 -->存在BUG
setprop persist.net.preferred.transports.order “1,0,3,2”

logcat -s NetworkRanker
11-08 02:05:57.555 705 849 I NetworkRanker: Preferred transports order set to: [0, 1, 3, 2]
11-08 02:05:57.555 705 849 I NetworkRanker: Confirm candidates transports order[0, 1, 3, 2]

这个修改是eth1一直作内网用
cd packages/modules/Connectivity

--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -742,6 +742,16 @@ public class EthernetNetworkFactory {
             if (mNetworkOfferCallback == null) {
                 mNetworkOfferCallback = new EthernetNetworkOfferCallback();
             }
+	
+	   if(name!=null && name.equals("eth1")){
+
+    		 //mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+     		 //mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+     		 mCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+     		 Log.d(TAG, "hcq mCapabilities: " + mCapabilities);
+	     }

如果编译报错,建议去掉framework/api/module-lib-current.txt,
make update-api 自己更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值