Appium appium setting 设置移动网络状态探究

陈恒捷 · 2015年10月08日 · 最后由 infy001 回复于 2015年10月10日 · 2871 次阅读

问题描述

在 android 5.0+ 上尝试通过 appium setting 设置移动网络状态时无效:

# python
from appium.webdriver.connectiontype import ConnectionType
self.driver.set_network_connection(ConnectionType.DATA_ONLY)

设置后不报错,但移动网络并没有启动。尝试过其他模式均没问题。

探究过程

先看源码:

https://github.com/appium/io.appium.settings/blob/master/src/io/appium/settings/handlers/DataService.java

...
private boolean setDataConnection(boolean on) {
    try {
      if (Build.VERSION.SDK_INT == Build.VERSION_CODES.FROYO) {
        Method dataConnSwitchmethod;
        Class<?> telephonyManagerClass;
        Object ITelephonyStub;
        Class<?> ITelephonyClass;

TelephonyManager telephonyManager = (TelephonyManager) mContext
.getSystemService(Context.TELEPHONY_SERVICE);

telephonyManagerClass = Class.forName(telephonyManager.getClass().getName());
Method getITelephonyMethod = telephonyManagerClass.getDeclaredMethod("getITelephony");
getITelephonyMethod.setAccessible(true);
ITelephonyStub = getITelephonyMethod.invoke(telephonyManager);
ITelephonyClass = Class.forName(ITelephonyStub.getClass().getName());

if (on) {
dataConnSwitchmethod = ITelephonyClass
.getDeclaredMethod("enableDataConnectivity");
} else {
dataConnSwitchmethod = ITelephonyClass
.getDeclaredMethod("disableDataConnectivity");
}
dataConnSwitchmethod.setAccessible(true);
dataConnSwitchmethod.invoke(ITelephonyStub);
} else {
//log.i("App running on Ginger bread+");
final ConnectivityManager conman = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
final Class<?> conmanClass = Class.forName(conman.getClass().getName());
final Field iConnectivityManagerField = conmanClass.getDeclaredField("mService");
iConnectivityManagerField.setAccessible(true);
final Object iConnectivityManager = iConnectivityManagerField.get(conman);
final Class<?> iConnectivityManagerClass = Class.forName(iConnectivityManager.getClass().getName());
final Method setMobileDataEnabledMethod = iConnectivityManagerClass.getDeclaredMethod("setMobileDataEnabled", Boolean.TYPE);
setMobileDataEnabledMethod.setAccessible(true);
setMobileDataEnabledMethod.invoke(iConnectivityManager, on);
}

return true;
} catch(Exception e) {
Log.e(TAG,"error turning on/off data: " + e.getMessage());
return false;
}
}
...


大致流程:
1. 判断是否是 android 2.2。如果是,使用 TelephonyManager 的 enableDataConnectivity 或 disableDataConnectivity 控制移动网络
2. 如果不是 2.2(即 2.3+),使用 ConnectivityManager 的 setMobileDataEnabled 控制移动网络

上面两个都是使用反射机制调用 android 的隐藏 api 实现的。

Google 一番,找到如下解答:
[Android L (5.x) Turn ON/OFF “Mobile Data” programmatically](http://stackoverflow.com/questions/29340150/android-l-5-x-turn-on-off-mobile-data-programmatically):

> In Android L 5.xx the hidden API setMobileDataEnabled method is
> removed and it can no longer be used. You can verify this in android
> lolipop source code under
> /frameworks/base/core/java/android/net/ConnectivityManager.java.
> 
> If you still insist to perform it, you can use code snippet answered
> by Kushal but getDataEnabled is a system api, which normal user
> applications cant access. There is also one more system api available
> setDataEnabled under TelephonyManager.
> (/frameworks/base/telephony/java/android/telephony/TelephonyManager.java)

即代码中在 android 2.3+ 上使用的方法已经无效(隐藏 api 已经被去掉)

# 解决方法

Stackoverflow 上找到两个看起来比较靠谱的方法(条件所限,目前均未试验过):

1、 调用另一个新的隐藏 API :`setDataEnabled` 。
> 参考<http://stackoverflow.com/questions/29340150/android-l-5-x-turn-on-off-mobile-data-programmatically>
```java
public void setMobileDataState(boolean mobileDataEnabled)
{
    try
    {
        TelephonyManager telephonyService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        Method setMobileDataEnabledMethod = telephonyService.getClass().getDeclaredMethod("setDataEnabled", boolean.class);

        if (null != setMobileDataEnabledMethod)
        {
            setMobileDataEnabledMethod.invoke(telephonyService, mobileDataEnabled);
        }
    }
    catch (Exception ex)
    {
        Log.e(TAG, "Error setting mobile data state", ex);
    }
}

public boolean getMobileDataState()
{
    try
    {
        TelephonyManager telephonyService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        Method getMobileDataEnabledMethod = telephonyService.getClass().getDeclaredMethod("getDataEnabled");

        if (null != getMobileDataEnabledMethod)
        {
            boolean mobileDataEnabled = (Boolean) getMobileDataEnabledMethod.invoke(telephonyService);

            return mobileDataEnabled;
        }
    }
    catch (Exception ex)
    {
        Log.e(TAG, "Error getting mobile data state", ex);
    }

    return false;
}

缺点: 需要 android.permission.MODIFY_PHONE_STATE 权限(这是系统级权限,普通 app 获取不到,系统 app 才能拿到)

2、调用 shell 命令

参考http://stackoverflow.com/questions/26539445/the-setmobiledataenabled-method-is-no-longer-callable-as-of-android-l-and-later

public static void setMobileNetworkfromLollipop(Context context) throws Exception {
    String command = null;
    int state = 0;
    try {
        // Get the current state of the mobile network.
        state = isMobileDataEnabledFromLollipop(context) ? 0 : 1;
        // Get the value of the "TRANSACTION_setDataEnabled" field.
        String transactionCode = getTransactionCode(context);
        // Android 5.1+ (API 22) and later.
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
            SubscriptionManager mSubscriptionManager = (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
            // Loop through the subscription list i.e. SIM list.
            for (int i = 0; i < mSubscriptionManager.getActiveSubscriptionInfoCountMax(); i++) {                    
                if (transactionCode != null && transactionCode.length() > 0) {
                    // Get the active subscription ID for a given SIM card.
                    int subscriptionId = mSubscriptionManager.getActiveSubscriptionInfoList().get(i).getSubscriptionId();
                    // Execute the command via `su` to turn off
                    // mobile network for a subscription service.
                    command = "service call phone " + transactionCode + " i32 " + subscriptionId + " i32 " + state;
                    executeCommandViaSu(context, "-c", command);
                }
            }
        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
            // Android 5.0 (API 21) only.
            if (transactionCode != null && transactionCode.length() > 0) {
                // Execute the command via `su` to turn off mobile network.                     
                command = "service call phone " + transactionCode + " i32 " + state;
                executeCommandViaSu(context, "-c", command);
            }
        }
    } catch(Exception e) {
        // Oops! Something went wrong, so we throw the exception here.
        throw e;
    }           
}

private static void executeCommandViaSu(Context context, String option, String command) {
boolean success = false;
String su = "su";
for (int i=0; i < 3; i++) {
// Default "su" command executed successfully, then quit.
if (success) {
break;
}
// Else, execute other "su" commands.
if (i == 1) {
su = "/system/xbin/su";
} else if (i == 2) {
su = "/system/bin/su";
}

try {
// Execute command as "su".
Runtime.getRuntime().exec(new String[]{su, option, command});
} catch (IOException e) {
success = false;
// Oops! Cannot execute su for some reason.
// Log error here.
} finally {
success = true;
}
}
}

缺点:需要 root 权限

# 总结

1. 学到了一种调用系统隐藏 API 的方法(反射)
2. 了解了如何能够设置数据连接的开关

报了一个 issue :<https://github.com/appium/io.appium.settings/issues/5>

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 5 条回复 时间 点赞

这个不好修了. 他们估计也只能在代码中加点警告了.

#1 楼 @seveniruby 对的,看起来 google 其实就是想收回这个权限,让普通 app 用不了。所以我也只是建议在文档里加警告。

这个打开移动网络的方式我采用了模拟用户操作的形式,下来顶栏,点击数据

#3 楼 @yuwuhen333 这是个不错的办法!赞!

有办法设置代理让模拟器能够连接网络吗?我指的是 APP。发现貌似不行!

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册