Android 9.0 以太网静态IP设置

6,141 阅读5分钟

吐槽一下,官方貌似没有以太网和WiFi的配置流程,网络设置这部分主要借鉴的是Settings中的内容,简单记录下,方便日后 copy

以太网配置需要使用EthernetManager类,我们看下这个类的特点:

/**
 * A class representing the IP configuration of the Ethernet network.
 *
 * @hide
 */
@SystemApi
@TestApi
@SystemService(Context.ETHERNET_SERVICE)
public class EthernetManager

这个类对普通应用并不开放,需要一些特别的权限

环境准备工作

  1. 进行静态网络设置需要先把app配置为系统级
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xxxx.xxxx"
    android:sharedUserId="android.uid.system">
  1. 由于使用到了EthernetManager类,这个类Android定义为隐藏的,所以我们需要依赖一个framework.jar
compileOnly files('frameworks.jar')

compileOnly即可

  • 此时在AS中还是会显示找不到EthernetManager
  • AS管理的每个module中有一个.yml文件
  • .yml文件中的framework.jar的顺序放到最前面就可以了

像这样

    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="library" name="Gradle: ./../../fm-libs/ironman_frameworks.jar" level="project" />
    //......
    <orderEntry type="module" module-name="factory_test_interf" />
    <orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />

这样,AS就会优先查找framework.jar中的类。

如果启动编译,应该还会出现找不到类的情况,此时我们还需要在Projectbuild.gradle中进行如下配置:

allprojects {
    repositories {
        //......
    }
    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
            //......
            options.compilerArgs.add('-Xbootclasspath/p:/Users/lijie/fm-libs/frameworks.jar')
        }
    }
}

这样编译时应该就会查找framework.jar中的类了

代码开始

  1. 一个接口类NetworkConfiguration,从Settings中借鉴过来的。
import android.net.IpConfiguration;
import android.net.wifi.WifiManager;
/**
 * Provide unified interface for network configuration that works for both Wi-Fi and Ethernet.
 */
public interface NetworkConfiguration {
    /**
     * Set IpConfiguration
     *
     * @param configuration IpConfiguration to set
     */
    public void setIpConfiguration(IpConfiguration configuration);
    /**
     * Get IpConfiguration
     *
     * @return IpConfiguration
     */
    public IpConfiguration getIpConfiguration();
    /**
     * Save current network configuration to system
     *
     * @param listener listener to notify the result
     */
    public void save(WifiManager.ActionListener listener);
    /**
     * Get printable name for this network.
     *
     * @return Printable name
     */
    public String getPrintableName();
}
  1. 以太网配置实现类EthernetConfig,实现了NetworkConfiguration接口
import android.content.Context;
import android.net.EthernetManager;
import android.net.IpConfiguration;
import android.net.wifi.WifiManager;
import android.os.Looper;
/**
 * Ethernet configuration that implements NetworkConfiguration.
 */
class EthernetConfig implements NetworkConfiguration {
    private EthernetManager mEthernetManager;
    private IpConfiguration mIpConfiguration;
    private String mName;
    private String mInterfaceName;
    EthernetConfig(Context context) {
        if (Thread.currentThread().getId() != context.getMainLooper().getThread().getId()) {
            // 由于 EthernetManager 在初始化的时候会启动一个Handler
            // 如果不是在主线程中,并且没有调用Looper.prepare(),会出现异常
            if (Looper.myLooper() == null) {
                Looper.prepare();
            }
        }
        mEthernetManager = (EthernetManager) context.getSystemService(Context.ETHERNET_SERVICE);
        mIpConfiguration = new IpConfiguration();
        mName = "Ethernet";
    }
    @Override
    public IpConfiguration getIpConfiguration() {
        return mIpConfiguration;
    }
    @Override
    public void setIpConfiguration(IpConfiguration configuration) {
        mIpConfiguration = configuration;
    }
    @Override
    public void save(WifiManager.ActionListener listener) {
        if (mInterfaceName != null) {
            mEthernetManager.setConfiguration(mInterfaceName, mIpConfiguration);
        }

        if (listener != null) {
            listener.onSuccess();
        }
    }
    /**
     * Load IpConfiguration from system.
     */
    public void load() {
        String[] ifaces = mEthernetManager.getAvailableInterfaces();
        if (ifaces.length > 0) {
            mInterfaceName = ifaces[0];
            mIpConfiguration = mEthernetManager.getConfiguration(mInterfaceName);
        }
    }

    @Override
    public String getPrintableName() {
        return mName;
    }
}
  1. 配置流程工具类EthernetUtils,注释很详细了哈
import android.content.Context;
import android.net.IpConfiguration;
import android.net.LinkAddress;
import android.net.StaticIpConfiguration;
import android.net.wifi.WifiManager;
import android.text.TextUtils;
import android.util.Log;

import java.net.Inet4Address;
import java.util.concurrent.CountDownLatch;

import static com.fengmi.factory_test_interf.sdk_utils.NetworkUtil.netMask2Length;

public final class EthernetUtils {
    private static final String TAG = "EthernetUtils";
    private static volatile boolean res = false;
    private EthernetUtils() {
    }
    /**
     * 设置 IP 获取方式为 DHCP 动态分配
     *
     * @param context 上下文对象
     * @return 配置是否保存成功
     */
    public static boolean saveDHCPEthernetConfig(Context context) {
        res = false;
        //1. 创建一个EthernetConfig对象
        EthernetConfig netConfig = new EthernetConfig(context);
        //2. load 加载系统配置到对象中
        netConfig.load();
        //3. 设置IP获取方式为 DHCP
        netConfig.getIpConfiguration().setIpAssignment(IpConfiguration.IpAssignment.DHCP);
        //4. 设置一个等待latch
        CountDownLatch latch = new CountDownLatch(1);
        //5. 保存配置到系统,并监听是否设置成功
        netConfig.save(new WifiManager.ActionListener() {
            @Override
            public void onSuccess() {
                Log.d(TAG, "set IP assignment DHCP success");
                res = true;
                latch.countDown();
            }

            @Override
            public void onFailure(int i) {
                Log.d(TAG, "set IP assignment DHCP failed, code=" + i);
                res = false;
                latch.countDown();
            }
        });

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return res;
    }
    /**
     * 设置以太网静态IP<p/>
     * 网上都没能找到正规的流程说明,从Settings中借鉴来的代码<p/>
     * 人性化的我添加了注释,吼吼吼<p/>
     * 网络配置相关的科普链接{https://blog.51cto.com/zhoutao/93629}
     *
     * @param context   上下文
     * @param ipAddress ip 地址,格式为:10.10.10.10
     * @param netMask   子网掩码,格式为:255.255.255.0
     * @param gateway   默认网关,格式为:10.10.10.1
     * @param dns       dns服务地址,google 提供了两个免费的
     * @return 配置是否成功
     */
    public static boolean saveStaticEthernetConfig(Context context,
                                                   String ipAddress,
                                                   String netMask,
                                                   String gateway,
                                                   String dns) {
        res = false;
        // 创建一个 StaticIpConfiguration 对象,用来保存静态IP相关的信息
        StaticIpConfiguration staticConfig = new StaticIpConfiguration();
        // 创建一个EthernetConfig对象
        EthernetConfig netConfig = new EthernetConfig(context);
        // load 加载系统配置到对象中
        netConfig.load();
        // 关联配置信息对象
        netConfig.getIpConfiguration().setStaticIpConfiguration(staticConfig);

        // 针对 IP地址 做一些异常判断
        if (TextUtils.isEmpty(ipAddress)) {
            Log.i(TAG, "empty ipv4 addr");
            return false;
        }

        // 根据子网掩码计算Prefix Length,看看究竟掩护了多少位
        // 其实就是计算子网掩码比特位为1的位数。
        int networkPrefixLength;
        try {
            networkPrefixLength = netMask2Length(netMask);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        //根据 IP 和 子网掩码的Prefix Length信息 生成 ipAddress 对象
        Inet4Address inetAddr;
        try {
            // 将IP 转化为 Inet4Address 对象
            inetAddr = (Inet4Address) android.net.NetworkUtils.numericToInetAddress(ipAddress);
            // 将 `Inet4Address 对象` 和 `子网掩码的Prefix Length信息` 打包成LinkAddress对象
            // 赋值给 StaticIpConfiguration 对象中的 ipAddress
            staticConfig.ipAddress = new LinkAddress(inetAddr, networkPrefixLength);
        } catch (IllegalArgumentException | ClassCastException e) {
            e.printStackTrace();
            return false;
        }

        // 将网关信息设置给 StaticIpConfiguration 对象中的 gateway
        if (!TextUtils.isEmpty(gateway)) {
            try {
                staticConfig.gateway =
                        android.net.NetworkUtils.numericToInetAddress(gateway);
            } catch (IllegalArgumentException | ClassCastException e) {
                e.printStackTrace();
                return false;
            }
        }

        //将 dns 添加到 StaticIpConfiguration 对象的 DNS 服务列表中
        if (!TextUtils.isEmpty(dns)) {
            try {
                staticConfig.dnsServers.add(
                        android.net.NetworkUtils.numericToInetAddress(dns));
            } catch (IllegalArgumentException | ClassCastException e) {
                e.printStackTrace();
                return false;
            }
        }

        // 添加两个Google提供的免费DNS服务器的IP地址
        try {
            staticConfig.dnsServers.add(
                    android.net.NetworkUtils.numericToInetAddress("8.8.8.8"));
            staticConfig.dnsServers.add(
                    android.net.NetworkUtils.numericToInetAddress("8.8.4.4"));
        } catch (IllegalArgumentException | ClassCastException e) {
            e.printStackTrace();
            return false;
        }

        // 设置 IpAssignment 为 Static 模式
        netConfig.getIpConfiguration().setIpAssignment(IpConfiguration.IpAssignment.STATIC);
        // 设置一个等待latch
        CountDownLatch latch = new CountDownLatch(1);
        // 保存配置信息到系统,并监听设置结果
        netConfig.save(new WifiManager.ActionListener() {
            @Override
            public void onSuccess() {
                Log.d(TAG, "set static IP assignment success");
                res = true;
                latch.countDown();
            }

            @Override
            public void onFailure(int i) {
                Log.d(TAG, "set static IP assignment failed, code=" + i);
                res = false;
                latch.countDown();
            }
        });

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return res;
    }
    /**
     * 计算子网掩码的 Prefix Length
     *
     * @param netMask 子网掩码
     * @return Prefix Length
     */
    private static int netMask2Length(String netMask) {
        byte[] array = netMask2ByteArray(netMask);
        if (array == null) {
            return -1;
        }
        int i = array[0] << 24 | array[1] << 16 | array[2] << 8 | array[3];
        int j = 32;
        int k = 1;
        while (((i & k) == 0) && (k != 0)) {
            k <<= 1;
            j--;
        }
        return j;
    }
    /**
     * 将子网掩码转化为字节数组
     *
     * @param str 子网掩码
     * @return 字符数组,格式不正确返回null
     */
    private static byte[] netMask2ByteArray(String str) {
        if (str == null || str.length() == 0) {
            return null;
        }
        StringTokenizer tokenizer = new StringTokenizer(str, ".");
        if (tokenizer.countTokens() != 4) {
            return null;
        }
        byte[] arrayOfByte = new byte[4];
        for (int i = 0; tokenizer.hasMoreTokens(); i++) {
            int j = Integer.parseInt(tokenizer.nextToken());
            arrayOfByte[i] = (byte) j;
        }
        return arrayOfByte;
    }
}

总结

想要完成这个功能

  • EthernetManager这种隐藏类的导入上需要费些时间
  • 熟悉网络配置相关的名词需要些时间
    • IP 地址
    • 子网掩码
    • 默认网关
    • dns
  • 熟悉配置流程需要些时间
    • 首先加载系统网络配置
    • 然后设置对应的网络参数
    • 然后设置不同的类型
      • DHCP
      • STATIC
    • 最后保存配置

实测可用,WiFi的实现和这个流程也差不多,功能虽然已经完成,但是在流程上还有些疑惑,就不先贴出来了,嘻嘻!