从 Android L 及更高版本开始, setMobileDataEnabled 方法不再可调用 [英] The setMobileDataEnabled method is no longer callable as of Android L and later

查看:21
本文介绍了从 Android L 及更高版本开始, setMobileDataEnabled 方法不再可调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已登录 问题 78084 与 Google 有关 setMobileDataEnabled() 方法不再可通过反射调用.从 Android 2.1 (API 7) 到 Android 4.4 (API 19),它可以通过反射调用,但从 Android L 和更高版本开始,即使使用 root,setMobileDataEnabled() 方法也无法调用.>

官方回应是问题已关闭",状态设置为WorkingAsIntended".谷歌的简单解释是:

<块引用>

私有 API 是私有的,因为它们不稳定并且可能会在没有通知的情况下消失.

是的,谷歌,我们已经意识到使用反射调用隐藏方法的风险——甚至在 Android 出现之前——但你需要提供一个更可靠的答案,如果有的话,以实现相同的结果如 setMobileDataEnabled().(如果您和我一样对 Google 的决定不满意,请登录 Issue 78084 并尽可能多地给它加注星标,让 Google 知道错误他们的方式.)

那么,我要问你的问题是:在 Android 设备上以编程方式启用或禁用移动网络功能时,我们是否处于死胡同?不知何故,这种来自 Google 的严厉方法并不适合我.如果您有适用于 Android 5.0 (Lollipop) 及更高版本的解决方法,我很乐意在此线程中听到您的回答/讨论.

我已经使用下面的代码来查看 setMobileDataEnabled() 方法是否可用:

final ClassconmanClass = Class.forName(context.getSystemService(Context.CONNECTIVITY_SERVICE).getClass().getName());final Field iConnectivityManagerField = conmanClass.getDeclaredField("mService");iConnectivityManagerField.setAccessible(true);最终对象 iConnectivityManager = iConnectivityManagerField.get(context.getSystemService(Context.CONNECTIVITY_SERVICE));最终类iConnectivityManagerClass = Class.forName(iConnectivityManager.getClass().getName());final Method[] methods = iConnectivityManagerClass.getDeclaredMethods();for(最终方法方法:方法){if (method.toGenericString().contains("set")) {Log.i("TESTING", "方法:" + method.getName());}}

但事实并非如此.

更新:目前,如果设备已 root,则可以切换移动网络.但是,对于非root设备,由于没有通用的方法来切换移动网络,这仍然是一个调查过程.

解决方案

为了扩展 Muzikant 的解决方案 #2,有人可以在 Android 5.0 根设备上尝试以下解决方案(因为我目前没有)和让我知道它是否有效.

要启用或禁用移动数据,请尝试:

//1:启用;0:禁用su -c 设置将全局 mobile_data 设为 1su -c am 广播 -a android.intent.action.ANY_DATA_STATE --ez 状态 1

注意:mobile_data 变量可以在 /android-sdk/sources/android-21/android/provider/Settings.java 的 Android API 21 源代码中找到并声明为:

/*** 用户是否允许移动数据连接.看* ConnectivityManager 了解更多信息.* @隐藏*/public static final String MOBILE_DATA = "mobile_data";

虽然 android.intent.action.ANY_DATA_STATE Intent 可以在 /android-sdk/sources/android-21/com/android/internal/的 Android API 21 源代码中找到Telephony/TelephonyIntents.java 并声明为:

/*** 广播动作:任何一个的数据连接状态已经改变* 手机的移动数据连接(例如,默认、彩信或 GPS 特定连接).** <p class="note">* 需要 READ_PHONE_STATE 权限.* <p class="note">这是一个受保护的意图,只能由系统发送.**/public static final String ACTION_ANY_DATA_CONNECTION_STATE_CHANGED= "android.intent.action.ANY_DATA_STATE";

UPDATE 1:如果您不想在 Android 应用程序中实现上述 Java 代码,那么您可以通过 shell (Linux) 运行 su 命令或命令提示符 (Windows) 如下:

adb shell "su -c 'settings put global mobile_data 1; am broadcast -a android.intent.action.ANY_DATA_STATE --ez state 1'"

注意:adb 位于 /android-sdk/platform-tools/ 目录.settings 命令仅在 Android 4.2 或更高版本上受支持.较旧的 Android 版本会报告 "sh: settings: not found" 错误.

更新 2:在 root Android 5+ 设备上切换移动网络的另一种方法是使用未记录的 service shell 命令.可以通过 ADB 执行以下命令来切换移动网络:

//1:启用;0:禁用adb shell "su -c 'service call phone 83 i32 1'"

或者只是:

//1:启用;0:禁用adb shell service call phone 83 i32 1

注意 1:service call phone 命令中使用的交易代码 83 可能会因 Android 版本而异.请检查 com.android.internal.telephony.ITelephony 以了解您的 Android 版本的 TRANSACTION_setDataEnabled 字段的值.此外,您最好使用反射来获取 TRANSACTION_setDataEnabled 字段的值,而不是硬编码 83.这样,它将适用于所有运行 Android 5+ 的移动品牌(如果您不知道如何使用反射来获取 TRANSACTION_setDataEnabled 字段的值,请参阅下面 PhongLe 的解决方案 - 拯救我在此处复制.)重要:请注意事务代码TRANSACTION_setDataEnabled 仅在 Android 5.0 及更高版本中引入.由于事务代码 TRANSACTION_setDataEnabled 不存在,因此在较早版本的 Android 上运行此事务代码将无济于事.

注意 2:adb 位于 /android-sdk/platform-tools/ 目录.如果您不想使用 ADB,请在您的应用中通过 su 执行该方法.

注意 3:请参阅下面的更新 3.

更新 3:许多 Android 开发人员通过电子邮件向我发送了有关为 Android 5+ 打开/关闭移动网络的问题,但我不会回复个人电子邮件,而是将我的答案发布在这里,以便每个人都可以使用并针对他们的 Android 应用进行调整.

首先,让我们澄清一些关于以下方面的误解和误解:

svc 数据使能svc 数据禁用

上述方法只会打开/关闭后台数据,不会订阅服务,因此电池会消耗相当多,因为订阅服务 - 一个 Android 系统服务 - 仍将运行背景.对于支持多张 SIM 卡的 Android 设备,这种情况更糟,因为订阅服务会不断扫描可用的移动网络以与 Android 设备中可用的活动 SIM 卡一起使用.使用此方法风险自负.

现在,关闭移动网络的正确方法,包括通过API 22 中引入的 SubscriptionManager 类,是:

public static void setMobileNetworkfromLollipop(Context context) 抛出异常 {字符串命令 = null;整数状态 = 0;尝试 {//获取移动网络的当前状态.状态 = isMobileDataEnabledFromLollipop(context) ?0 : 1;//获取TRANSACTION_setDataEnabled"字段的值.String transactionCode = getTransactionCode(context);//Android 5.1+ (API 22) 及更高版本.如果 (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {SubscriptionManager mSubscriptionManager = (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);//遍历订阅列表,即 SIM 列表.for (int i = 0; i  0) {//获取给定 SIM 卡的活动订阅 ID.int subscriptionId = mSubscriptionManager.getActiveSubscriptionInfoList().get(i).getSubscriptionId();//通过`su`执行命令关闭//用于订阅服务的移动网络.command = "service call phone" + transactionCode + " i32 " + subscriptionId + " i32 " + state;executeCommandViaSu(上下文,-c",命令);}}} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {//仅限 Android 5.0 (API 21).if (transactionCode != null && transactionCode.length() > 0) {//通过`su`执行命令关闭移动网络.命令=服务电话"+交易代码+i32"+状态;executeCommandViaSu(上下文,-c",命令);}}} 捕获(异常 e){//哎呀!出了点问题,所以我们在这里抛出异常.扔e;}}

要检查是否启用了移动网络:

private static boolean isMobileDataEnabledFromLollipop(Context context) {布尔状态 = 假;如果 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {state = Settings.Global.getInt(context.getContentResolver(), "mobile_data", 0) == 1;}返回状态;}

获取TRANSACTION_setDataEnabled字段的值(从下面PhongLe的解决方案中借用):

private static String getTransactionCode(Context context) throws Exception {尝试 {final TelephonyManager mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);最终类mTelephonyClass = Class.forName(mTelephonyManager.getClass().getName());最终方法 mTelephonyMethod = mTelephonyClass.getDeclaredMethod("getITelephony");mTelephonyMethod.setAccessible(true);最终对象 mTelephonyStub = mTelephonyMethod.invoke(mTelephonyManager);最终类mTelephonyStubClass = Class.forName(mTelephonyStub.getClass().getName());最终类mClass = mTelephonyStubClass.getDeclaringClass();final Field field = mClass.getDeclaredField("TRANSACTION_setDataEnabled");field.setAccessible(true);返回 String.valueOf(field.getInt(null));} 捕获(异常 e){//TRANSACTION_setDataEnabled"字段不可用,//或者在当前 API 级别中命名不同,所以我们抛出//异常并通知用户该方法不可用.扔e;}}

通过su执行命令:

private static void executeCommandViaSu(Context context, String option, String command) {布尔成功 = 假;字符串 su = "su";for (int i=0; i <3; i++) {//默认的su"命令执行成功,然后退出.如果(成功){休息;}//否则,执行其他su"命令.如果(我== 1){su = "/system/xbin/su";} else if (i == 2) {su = "/system/bin/su";}尝试 {//以su"的形式执行命令.Runtime.getRuntime().exec(new String[]{su, option, command});} catch (IOException e) {成功=假;//哎呀!由于某种原因无法执行`su`.//这里记录错误.} 最后 {成功=真;}}}

希望此更新能消除您对在已 root 的 Android 5+ 设备上打开/关闭移动网络可能存在的任何误解、误解或问题.

I have logged Issue 78084 with Google regarding the setMobileDataEnabled() method being no longer callable via reflection. It was callable since Android 2.1 (API 7) to Android 4.4 (API 19) via reflection, but as of Android L and later, even with root, the setMobileDataEnabled() method is not callable.

The official response is that the issue is "Closed" and the status set to "WorkingAsIntended". Google's simple explanation is:

Private APIs are private because they are not stable and might disappear without notice.

Yes, Google, we are aware of the risk of using reflection to call hidden method- even before Android came on the scene- but you need to provide a more solid answer as to alternatives, if any, for accomplishing the same result as setMobileDataEnabled(). (If you are displeased with Google's decision as I am, then log into Issue 78084 and star it as many as possible to let Google know the error of their way.)

So, my question to you is: Are we at a dead end when it comes to programmatically enable or disable mobile network function on an Android device? This heavy-handed approach from Google somehow does not sit well with me. If you have workaround for Android 5.0 (Lollipop) and beyond, I would love to hear your answer/discussion in this thread.

I have used the code below to see if the setMobileDataEnabled() method is available:

final Class<?> conmanClass = Class.forName(context.getSystemService(Context.CONNECTIVITY_SERVICE).getClass().getName());
final Field iConnectivityManagerField = conmanClass.getDeclaredField("mService");
iConnectivityManagerField.setAccessible(true);
final Object iConnectivityManager = iConnectivityManagerField.get(context.getSystemService(Context.CONNECTIVITY_SERVICE));
final Class<?> iConnectivityManagerClass = Class.forName(iConnectivityManager.getClass().getName());
final Method[] methods = iConnectivityManagerClass.getDeclaredMethods();
for (final Method method : methods) {
    if (method.toGenericString().contains("set")) {
        Log.i("TESTING", "Method: " + method.getName());
    }
}

But it's not.

UPDATE: Currently, it's possible to toggle mobile network if the device is rooted. However, for non-rooted devices, it's still an investigative process as there is no universal method to toggle mobile network.

解决方案

To extend Muzikant's Solution #2, can someone please try the solution below on an Android 5.0 rooted device (as I currently do not possess one) and let me know if it works or does not work.

To enable or disable mobile data, try:

// 1: Enable; 0: Disable
su -c settings put global mobile_data 1
su -c am broadcast -a android.intent.action.ANY_DATA_STATE --ez state 1

Note: The mobile_data variable can be found in Android API 21 source codes at /android-sdk/sources/android-21/android/provider/Settings.java and is declared as:

/**
 * Whether mobile data connections are allowed by the user.  See
 * ConnectivityManager for more info.
 * @hide
*/
public static final String MOBILE_DATA = "mobile_data";

While the android.intent.action.ANY_DATA_STATE Intent can be found in Android API 21 source codes at /android-sdk/sources/android-21/com/android/internal/telephony/TelephonyIntents.java and is declared as:

/**
 * Broadcast Action: The data connection state has changed for any one of the
 * phone's mobile data connections (eg, default, MMS or GPS specific connection).
 *
 * <p class="note">
 * Requires the READ_PHONE_STATE permission.
 * <p class="note">This is a protected intent that can only be sent by the system.
 *
 */
public static final String ACTION_ANY_DATA_CONNECTION_STATE_CHANGED
        = "android.intent.action.ANY_DATA_STATE";

UPDATE 1: If you don't want to implement the above Java codes in your Android application, then you can run the su commands via a shell (Linux) or command prompt (Windows) as follow:

adb shell "su -c 'settings put global mobile_data 1; am broadcast -a android.intent.action.ANY_DATA_STATE --ez state 1'"

Note: adb is located at /android-sdk/platform-tools/ directory. The settings command is only supported on Android 4.2 or later. Older Android version will report a "sh: settings: not found" error.

UPDATE 2: Another way to toggle mobile network on a rooted Android 5+ device would be to use the undocumented service shell command. The following command can be executed via ADB to toggle mobile network:

// 1: Enable; 0: Disable
adb shell "su -c 'service call phone 83 i32 1'"

Or just:

// 1: Enable; 0: Disable
adb shell service call phone 83 i32 1

Note 1: The transaction code 83 used in the service call phone command might change between Android versions. Please check com.android.internal.telephony.ITelephony for the value of the TRANSACTION_setDataEnabled field for your version of Android. Also, instead of hardcoding 83, you would be better off using Reflection to get the value of the TRANSACTION_setDataEnabled field. This way, it will work across all mobile brands running Android 5+ (If you don't know how to use Reflection to get the value of the TRANSACTION_setDataEnabled field, see solution from PhongLe below- save me from duplicating it here.) Important: Please note that transaction code TRANSACTION_setDataEnabled has only been introduced in Android 5.0 and later versions. Running this transaction code on earlier versions of Android will do nothing as the transaction code TRANSACTION_setDataEnabled does not exist.

Note 2: adb is located at /android-sdk/platform-tools/ directory. If you do not wish to use ADB, execute the method via su in your app.

Note 3: See UPDATE 3 below.

UPDATE 3: Many Android developers have emailed me questions regarding switching mobile network on/off for Android 5+, but instead of answering individual emails, I'll post my answer here so everyone can use it and adapt it for their Android apps.

First thing first, let's clear up some misconception and misunderstanding regarding:

svc data enable
svc data disable

The above methods would only turn background data on/off, not the subscription service, so the battery will drain a fair bit since the subscription service- an Android system service- will still be running in the background. For Android devices supporting multiple sim cards, this scenario is worse as the subscription service constantly scans for available mobile network(s) to use with the active SIM cards available in the Android device. Use this method at your own risk.

Now, the proper way to switch off mobile network, including its corresponding subscription service via the SubscriptionManager class introduced in API 22, is:

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;
    }           
}

To check if the mobile network is enabled or not:

private static boolean isMobileDataEnabledFromLollipop(Context context) {
    boolean state = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        state = Settings.Global.getInt(context.getContentResolver(), "mobile_data", 0) == 1;
    }
    return state;
}

To get the value of the TRANSACTION_setDataEnabled field (borrowed from PhongLe's solution below):

private static String getTransactionCode(Context context) throws Exception {
    try {
        final TelephonyManager mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 
        final Class<?> mTelephonyClass = Class.forName(mTelephonyManager.getClass().getName());
        final Method mTelephonyMethod = mTelephonyClass.getDeclaredMethod("getITelephony");
        mTelephonyMethod.setAccessible(true);
        final Object mTelephonyStub = mTelephonyMethod.invoke(mTelephonyManager);
        final Class<?> mTelephonyStubClass = Class.forName(mTelephonyStub.getClass().getName());
        final Class<?> mClass = mTelephonyStubClass.getDeclaringClass();
        final Field field = mClass.getDeclaredField("TRANSACTION_setDataEnabled");
        field.setAccessible(true);
        return String.valueOf(field.getInt(null));
    } catch (Exception e) {
        // The "TRANSACTION_setDataEnabled" field is not available,
        // or named differently in the current API level, so we throw
        // an exception and inform users that the method is not available.
        throw e;
    }
}

To execute command via su:

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;
        }
    }
}

Hope this update clears up any misconception, misunderstanding, or question you may have about switching mobile network on/off on rooted Android 5+ devices.

这篇关于从 Android L 及更高版本开始, setMobileDataEnabled 方法不再可调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆