从Android L及更高版本开始,setMobileDataEnabled方法不再可调用

时间:2014-10-23 23:57:15

标签: android reflection android-5.0-lollipop

我已向Google记录Issue 78084,因为setMobileDataEnabled()方法无法通过反射调用。它可以通过反射从Android 2.1(API 7)到Android 4.4(API 19)进行调用,但是从Android L及更高版本开始,即使使用root,setMobileDataEnabled()方法也无法调用。

官方回复是问题是“已关闭”,状态设置为“WorkingAsIntended”。谷歌的简单解释是:

  

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

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

所以,我的问题是:在Android设备上以编程方式启用或禁用移动网络功能时,我们是否处于死胡同?谷歌的这种严厉的方法在某种程度上并不适合我。如果您有Android 5.0(Lollipop)及其他方法的解决方法,我很乐意听到您在此主题中的回答/讨论。

我使用了以下代码来查看setMobileDataEnabled()方法是否可用:

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

但事实并非如此。

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

9 个答案:

答案 0 :(得分:25)

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

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

// 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

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

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

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

/**
 * 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";

更新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/目录。仅在Android 4.2或更高版本上支持settings命令。较旧的Android版本会报告"sh: settings: not found"错误。

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

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

或者只是:

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

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

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

注3 :见下面的更新3。

更新3 :许多Android开发人员都通过电子邮件向我发送了有关为Android 5+开启/关闭移动网络的问题,但我没有回复个别电子邮件,而是在此发布我的答案,以便每个人可以使用它并适应他们的Android应用程序。

首先,让我们澄清一些误解和误解:

svc data enable
svc data disable

上述方法只会打开/关闭后台数据,订阅服务,因此,由于订阅服务(Android系统服务)仍将在的背景。对于支持多个SIM卡的Android设备,这种情况更糟,因为订阅服务会不断扫描可用的移动网络,以便与Android设备中提供的有效SIM卡一起使用。使用此方法需要您自担风险。

现在,通过API 22中引入的SubscriptionManager类关闭移动网络(包括其相应的订阅服务)的正确方法是:

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

获取TRANSACTION_setDataEnabled字段的值(借用PhongLe&#39; s解决方案):

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

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

希望此更新能够消除您在有根据的Android 5+设备上开启/关闭移动网络时的任何误解,误解或疑问。

答案 1 :(得分:9)

我注意到ChuongPham发布的服务调用方法并不能在所有设备上保持一致。

我发现以下解决方案,我认为在所有ROOTED设备上都可以正常工作。

通过 su

执行以下操作

启用移动数据

svc data enable

禁用移动数据

svc data disable

我认为这是最简单,最好的方法。

修改: 2个投票是我认为是商业原因。这个人现在删除了他的评论。自己动手吧,它有效! 还有人在评论中确认了这些工作。

答案 2 :(得分:6)

只是为了分享更多见解和可能的解决方案(适用于有根设备和系统应用)。

解决方案#1

setMobileDataEnabled似乎不再存在ConnectivityManager方法,并且此功能已使用两种方法TelephonyManagergetDataEnabled移至setDataEnabled。 我尝试用反射调用这些方法,如下面的代码所示:

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

执行代码时,您会SecurityException说明Neither user 10089 nor current process has android.permission.MODIFY_PHONE_STATE.

所以,是的,这是对内部API的预期更改,不再适用于在以前版本中使用该hack的应用程序。

(开始咆哮:那个可怕的android.permission.MODIFY_PHONE_STATE权限......结束咆哮)。

好消息是,如果您正在构建可以获得MODIFY_PHONE_STATE权限的应用程序(只有系统应用程序可以使用该权限),您可以使用上述代码切换移动数据状态。

解决方案#2

要检查移动数据的当前状态,您可以使用mobile_data的{​​{1}}字段(官方文档中未记录)。

Settings.Global

要启用/禁用移动数据,您可以在root设备上使用shell命令(只执行基本测试,以便评论中的任何反馈)。 您可以以root身份运行以下命令(1 =启用,0 =禁用):

Settings.Global.getInt(contentResolver, "mobile_data");

答案 3 :(得分:3)

我没有足够的声誉来评论,但我已经尝试了所有的答案并找到了以下内容:

ChuongPham:我没有使用 83 ,而是使用反射从TRANSACTION_setDataEnabled获取变量com.android.internal.telephony.ITelephony的值,因此它适用于所有Android 5+设备,无论如何品牌。

Muzikant:如果应用程序被移动到/system/priv-app/目录(感谢 rgruet ),则可以工作。否则,它也可以通过root工作!您只需要通知您的用户,在移动网络发生变化之前,应用需要重新启动。

AJ:工作 - 有点。不关闭订阅服务,所以我测试的设备耗尽了他们的电池。尽管声称声明,AJ的解决方案等同于Muzikant的解决方案。我可以通过调试不同的三星,索尼和LG股票ROM(我彻底)来证实这一点,并且可以反驳AJ声称他的解决方案与Muzikant相同的说法。 (注意:我无法获得一些Nexus和Motorola ROM,因此无法使用建议的解决方案测试这些ROM。)

无论如何,希望它能解决对解决方案的任何疑问。

快乐的编码! PL,德国

更新:对于那些想知道如何通过反射获取TRANSACTION_setDataEnabled字段值的人,您可以执行以下操作:

private static String getTransactionCodeFromApi20(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;
    }
}

答案 4 :(得分:3)

我发现su -c 'service call phone 83 i32 1'解决方案对于有根设备最可靠。感谢Phong Le的参考,我通过使用反射获取特定于供应商/操作系统的事务代码来改进它。也许它会对其他人有用。所以,这是源代码:

    public void changeConnection(boolean enable) {
        try{
            StringBuilder command = new StringBuilder();
            command.append("su -c ");
            command.append("service call phone ");
            command.append(getTransactionCode() + " ");
            if (Build.VERSION.SDK_INT >= 22) {
                SubscriptionManager manager = SubscriptionManager.from(context);
                int id = 0;
                if (manager.getActiveSubscriptionInfoCount() > 0)
                    id = manager.getActiveSubscriptionInfoList().get(0).getSubscriptionId();
                command.append("i32 ");
                command.append(String.valueOf(id) + " ");
            }
            command.append("i32 ");
            command.append(enable?"1":"0");
            command.append("\n");
            Runtime.getRuntime().exec(command.toString());
        }catch(IOException e){
            ...
        }
    }

    private String getTransactionCode() {
        try {
            TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            Class telephonyManagerClass = Class.forName(telephonyManager.getClass().getName());
            Method getITelephonyMethod = telephonyManagerClass.getDeclaredMethod("getITelephony");
            getITelephonyMethod.setAccessible(true);
            Object ITelephonyStub = getITelephonyMethod.invoke(telephonyManager);
            Class ITelephonyClass = Class.forName(ITelephonyStub.getClass().getName());

            Class stub = ITelephonyClass.getDeclaringClass();
            Field field = stub.getDeclaredField("TRANSACTION_setDataEnabled");
            field.setAccessible(true);
            return String.valueOf(field.getInt(null));
        } catch (Exception e) {
            if (Build.VERSION.SDK_INT >= 22)
                return "86";
            else if (Build.VERSION.SDK_INT == 21)
                return "83";
        }
        return "";
    }

<强>更新

我的一些用户报告他们在通过此方法开启移动网络时遇到问题(关闭工作正常)。有人有解决方案吗?

<强> UPDATE2:

在挖掘了Android 5.1代码后,我发现他们改变了交易签名。 Android 5.1带来了对多SIM卡的官方支持。因此,事务需要所谓的订阅ID作为第一个参数(read more here)。这种情况的结果是命令su -c 'service call phone 83 i32 1'无法在Android 5.1上启用Mobile Net。因此,Android 5.1上的完整命令应该像这样su -c 'service call phone 83 i32 0 i32 1'i32 0是subId,i32 1是命令0 - off和1 - on)。我已使用此修复程序更新上面的代码。

答案 5 :(得分:2)

如果您通过将.apk移动到/system/priv-app/文件夹,将移动到/system/app/文件夹来创建应用程序“系统”,那么Muzikant的解决方案#1似乎有效( @jaumard:也许这就是你的测试不起作用的原因。)

当.apk位于/system/priv-app/文件夹中时,它可以成功请求清单中的可怕android.permission.MODIFY_PHONE_STATE权限并致电TelephonyManager.setDataEnabledTelephonyManager.getDataEnabled

至少适用于Nexus 5 / Android 5.0。 .apk perms是0144。您需要重新启动设备才能将更改考虑在内,也许这可以避免 - 请参阅this thread

答案 6 :(得分:1)

纠正Muzikant Solution#2

settings put global mobile_data 1

仅启用移动数据的切换,但不对连接执行任何操作。仅启用了切换功能。为了使用

获取数据
su -c am broadcast -a android.intent.action.ANY_DATA_STATE --ez state 1

将错误作为

的额外内容
android.intent.action.ANY_DATA_STATE

需要String对象,而--ez参数用于布尔值。参考:PhoneGlobals.java&amp; PhoneConstants.java。使用连接或连接作为额外使用命令后

su -c am broadcast -a android.intent.action.ANY_DATA_STATE --es state connecting

仍然无法启用数据。

答案 7 :(得分:1)

并非所有手机和android版本都具有相同的启用/禁用移动数据功能。 否则,该解决方案将在我的手机(SAMSUNG SM-J100H)上进行测试

要启用移动数据:

adb shell service call phone 27

要禁用移动数据:

adb shell service call phone 28

答案 8 :(得分:0)

我从@ChuongPham和@ A.J派生了最终代码。用于启用和禁用蜂窝数据。如果启用,您可以调用 setMobileDataEnabled(true); ,对于禁用,您可以调用 setMobileDataEnabled(false);

public void setMobileDataEnabled(boolean enableOrDisable) throws Exception {
    String command = null;
    if (enableOrDisable) {
        command = "svc data enable";
    } else {
        command = "svc data disable";
    }


    executeCommandViaSu(mContext, "-c", command);
}

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