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

标签 android reflection android-5.0-lollipop

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

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

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



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

所以,我的问题是:在以编程方式启用或禁用 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());
    }
}

但事实并非如此。

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

最佳答案

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

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

// 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 Intent 可以在 /android-sdk/sources/android-21/com/android/internal/telephony/TelephonyIntents.java 的 Android API 21 源代码中找到。并声明为:
/**
 * 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代码,那么你可以运行su通过 shell (Linux) 或命令提示符 (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外壳命令。可以通过 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 :交易代码 83 用于service call phone命令可能会在 Android 版本之间发生变化。请查看com.android.internal.telephony.ITelephony对于 TRANSACTION_setDataEnabled 的值您的 Android 版本的字段。此外,而不是硬编码 83 ,您最好使用反射来获取 TRANSACTION_setDataEnabled 的值 field 。这样,它将适用于所有运行 Android 5+ 的移动品牌(如果您不知道如何使用反射来获取 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 设备中可用的 Activity SIM 卡一起使用。使用此方法需要您自担风险。

现在,关闭移动网络的正确方法,包括通过 SubscriptionManager 的相应订阅服务API 22 中引入的类是:
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 的解决方案中借用):
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;
        }
    }
}

希望此更新能够消除您对在已 Root 的 Android 5+ 设备上打开/关闭移动网络的任何误解、误解或疑问。

关于android - 从 Android L 及更高版本开始,setMobileDataEnabled 方法不再可调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26539445/

相关文章:

c# - `typeof(T).IsAssignableFrom(x.GetType())` 可以安全地重写为 `x is T` 吗?

android - 创建新用户后设置

android - 保存叠加图像 + cameraPreview

安卓 : Simple web application crashes on links other then the http scheme

android - 以较长的间隔时间轮询数据库,哪种方法最好、最有效?

C# Generics - 根据对象类型找到正确的具体类

android - 将 Activity 包装在 fragment 中,或替代解决方案

.net - 获取当前方法的名称

android - 在安卓 Lollipop 上创建闹钟

android - 如何模仿 Material-design 凸起的按钮样式,即使是前 Lollipop (减去特殊效果)?