web-dev-qa-db-fra.com

La méthode setMobileDataEnabled n'est plus appelable à partir de Android L et versions ultérieures.

Je me suis connecté Numéro 78084 avec Google à propos de la méthode setMobileDataEnabled() n'étant plus appelable par réflexion. Il était appelable depuis Android 2.1 (API 7) à Android 4.4 (API 19) via réflexion, mais à partir d'Android L et ultérieur, même avec root, la méthode setMobileDataEnabled() n'est pas appelable.

La réponse officielle est que le problème est "Fermé" et que le statut est défini sur "WorkingAsIntended". L'explication simple de Google est la suivante:

Les API privées sont privées car elles ne sont pas stables et peuvent disparaître sans préavis.

Oui, Google, nous sommes conscients du risque que représente l'utilisation de la réflexion pour appeler une méthode cachée - avant même l'arrivée d'Android - mais vous devez fournir une réponse plus solide quant aux alternatives, le cas échéant, permettant d'atteindre le même résultat que setMobileDataEnabled(). (Si vous êtes mécontent de la décision de Google telle que je suis, connectez-vous à numéro 78084 et mettez-y en avant le plus grand nombre possible afin que Google sache l'erreur de leur part.)

Ma question est donc la suivante: sommes-nous dans l'impasse en ce qui concerne l'activation ou la désactivation par programme de la fonction de réseau mobile sur un appareil Android? Cette approche lourde de Google ne me convient pas du tout. Si vous avez une solution de contournement pour Android 5.0 (Lollipop) et au-delà, j'aimerais entendre votre réponse/discussion dans ce fil.

J'ai utilisé le code ci-dessous pour voir si la méthode setMobileDataEnabled() est disponible:

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

Mais ce n'est pas.

UPDATE: Il est actuellement possible de basculer sur le réseau mobile si le périphérique est enraciné. Cependant, pour les appareils non-enracinés, il s'agit toujours d'un processus d'investigation car il n'existe pas de méthode universelle pour basculer le réseau mobile.

46
ChuongPham

J'ai remarqué que la méthode d'appel de service publiée par ChuongPham ne fonctionne pas de manière uniforme sur tous les appareils.

J'ai trouvé la solution suivante qui, à mon avis, fonctionnera sans problème sur tous les appareils ROOTED. 

Exécutez ce qui suit via su

Pour activer les données mobiles

svc data enable

Pour désactiver les données mobiles

svc data disable

Je pense que c'est la méthode la plus simple et la meilleure.

Edit: 2 votes négatifs étaient pour ce que je crois être des raisons commerciales. La personne a supprimé son commentaire maintenant. Essayez vous-même, cela fonctionne! Également confirmé à travailler par les gars dans les commentaires.

9
A.J.

Juste pour partager quelques idées et solutions possibles (pour les appareils enracinés et les applications système).

Solution n ° 1

Il semble que la méthode setMobileDataEnabled n'existe plus dans ConnectivityManager et que cette fonctionnalité a été déplacée vers TelephonyManager avec deux méthodes getDataEnabled et setDataEnabled. J'ai essayé d'appeler ces méthodes avec une réflexion comme vous pouvez le voir dans le code ci-dessous:

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

Lorsque vous exécutez le code, vous obtenez une SecurityException indiquant que Neither user 10089 nor current process has Android.permission.MODIFY_PHONE_STATE.

Donc, oui, il s'agit d'un changement intentionnel de l'API interne et n'est plus disponible pour les applications qui utilisaient ce hack dans les versions précédentes.

(Commencez la diatribe: cette terrible autorisation Android.permission.MODIFY_PHONE_STATE ... finissez la diatribe).

La bonne nouvelle est que si vous construisez une application pouvant acquérir l'autorisation MODIFY_PHONE_STATE (seules les applications système peuvent l'utiliser), vous pouvez utiliser le code ci-dessus pour basculer l'état des données mobiles.

Solution n ° 2

Pour vérifier l'état actuel des données mobiles, vous pouvez utiliser le champ mobile_data de Settings.Global (non documenté dans la documentation officielle).

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

Et pour activer/désactiver les données mobiles, vous pouvez utiliser les commandes Shell sur les appareils rootés (Un test de base suffit pour apprécier tout commentaire en retour) . Vous pouvez exécuter la ou les commandes suivantes en tant que root (1 = activer, 0 = désactiver):

settings put global mobile_data 1
settings put global mobile_data 0
6
Muzikant

J'ai trouvé que la solution su -c 'service call phone 83 i32 1' est la plus fiable pour les appareils enracinés. Grâce à Phong Le reference, je l’ai amélioré en obtenant le code de transaction spécifique au vendeur/os en utilisant la réflexion. Peut-être que ce sera utile pour quelqu'un d'autre. Donc, voici le code source:

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

Mettre à jour:

Certains de mes utilisateurs signalent qu'ils ont des difficultés à activer le réseau mobile via cette méthode (la désactivation fonctionne correctement). Quelqu'un at-il une solution?

Update2:

Après avoir creusé le code Android 5.1, j'ai constaté qu'ils avaient modifié la signature de la transaction. Android 5.1 apporte le support officiel de la multi-SIM. Ainsi, la transaction nécessite ce que l'on appelle un identifiant d'abonnement comme premier paramètre ( pour en savoir plus ici ). Le résultat de cette situation est que la commande su -c 'service call phone 83 i32 1' n'active pas Mobile Net sur Android 5.1. Ainsi, la commande complète sur Android 5.1 devrait être comme ceci su -c 'service call phone 83 i32 0 i32 1' (le i32 0 est le sous-id, le i32 1 est la commande 0 - off et 1 - on). J'ai mis à jour le code ci-dessus avec ce correctif.

3
nickkadrov

Je n'ai pas assez de réputation pour commenter, mais j'ai essayé toutes les réponses et trouvé ce qui suit:

ChuongPham: Au lieu d'utiliser 83 , j'ai utilisé la réflexion pour obtenir la valeur de la variable TRANSACTION_setDataEnabled à partir du com.Android.internal.telephony.ITelephony, de sorte qu'elle fonctionne sur tous les appareils Android 5+, quelles que soient leurs marques.

Muzikant: Fonctionne si l'application est déplacée dans le répertoire /system/priv-app/ (grâce à rgruet.) Sinon, cela fonctionne également via root! Vous devez simplement informer vos utilisateurs que l'application nécessitera un redémarrage avant que les modifications apportées au réseau mobile aient lieu.

AJ: en quelque sorte. N'éteint pas le service d'abonnement, alors les appareils que j'ai testés ont épuisé leurs batteries. La solution d'AJ estPASéquivalente à la solution de Muzikant malgré l'affirmation. Je peux le confirmer en déboguant différentes ROMs de Samsung, Sony et LG (je suis complet) et peut réfuter l'affirmation de AJ selon laquelle sa solution est la même que celle de Muzikant. (Remarque: je ne parviens pas à mettre la main sur certaines ROM Nexus et Motorola, je n'ai donc pas testé ces ROM avec les solutions proposées.)

Quoi qu'il en soit, espérons que cela dissipe tout doute sur les solutions.

Happy coding! PL, Allemagne

UPDATE: Pour ceux qui se demandent comment obtenir la valeur du champ TRANSACTION_setDataEnabled par réflexion, vous pouvez procéder comme suit:

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

La solution n ° 1 de Muzikant semble fonctionner si vous définissez l'application comme "système" en déplaçant le .apk vers le dossier /system/priv-app/, pas vers le fichier /system/app/ (@jaumard: c'est peut-être pourquoi votre test n'a pas fonctionné).

Lorsque le fichier .apk se trouve dans le dossier /system/priv-app/, il peut demander l'autorisation redoutable Android.permission.MODIFY_PHONE_STATE dans le manifeste et appeler TelephonyManager.setDataEnabled et TelephonyManager.getDataEnabled

Au moins, cela fonctionne sur Nexus 5/Android 5.0. Les permanentes .apk sont 0144. Vous devez redémarrer le périphérique pour que la modification soit prise en compte, peut-être que cela pourrait être évité - voir ce fil .

2
rgruet

Pour corriger la solution n ° 2 de Muzikant 

settings put global mobile_data 1

N'active que le basculement pour les données mobiles mais ne fait rien pour la connectivité. Seule la bascule est activée. Afin de faire fonctionner les données en utilisant

su -c am broadcast -a Android.intent.action.ANY_DATA_STATE --ez state 1

Donne l'erreur comme un extra pour 

Android.intent.action.ANY_DATA_STATE

Requiert l'objet String tandis que le paramètre --ez est utilisé pour booléen. Réf.: PhoneGlobals.Java & PhoneConstants.Java. Après avoir utilisé la connexion ou connecté en extra avec la commande

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

Ne fait toujours rien pour activer les données. 

1
Sahil Lombar

J'ai dérivé le code final de @ChuongPham et @ A.J. pour activer et désactiver les données cellulaires. pour activer, vous pouvez appeler setMobileDataEnabled (true); et pour désactiver vous pouvez appeler 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;
        }
    }
}
0
varotariya vajsi