Comment répondre par programme aux appels entrants sous Android 5.0 (Lollipop)?

87

Alors que j'essaie de créer un écran personnalisé pour les appels entrants, j'essaie de répondre par programme à un appel entrant. J'utilise le code suivant mais il ne fonctionne pas sous Android 5.0.

// Simulate a press of the headset button to pick up the call
Intent buttonDown = new Intent(Intent.ACTION_MEDIA_BUTTON);             
buttonDown.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
context.sendOrderedBroadcast(buttonDown, "android.permission.CALL_PRIVILEGED");

// froyo and beyond trigger on buttonUp instead of buttonDown
Intent buttonUp = new Intent(Intent.ACTION_MEDIA_BUTTON);               
buttonUp.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
context.sendOrderedBroadcast(buttonUp, "android.permission.CALL_PRIVILEGED");
maveroid
la source
Oh mec, pourquoi courir dans ça, glisse juste Man! me semble plus facile \ m /
nobalG
Je crée un écran d'appel entrant personnalisé pour les utilisateurs d'Android.
maveroid
2
N'importe qui? Je suis également intéressé par celui-ci! J'ai essayé beaucoup de choses, mais cela n'a pas fonctionné: /
Arthur
1
@nobalG dit-il par programme
attiré
1
@maveroid, avez-vous proposé une solution de contournement pour Android 5.0?
arthursfreire

Réponses:

155

Mise à jour avec Android 8.0 Oreo

Même si la question a été posée à l'origine pour la prise en charge d'Android L, les gens semblent toujours répondre à cette question et à cette réponse, il vaut donc la peine de décrire les améliorations apportées à Android 8.0 Oreo. Les méthodes rétrocompatibles sont toujours décrites ci-dessous.

Qu'est ce qui a changé?

À partir d' Android 8.0 Oreo , le groupe d'autorisations PHONE contient également l' autorisation ANSWER_PHONE_CALLS . Comme le nom de l'autorisation l'indique, le maintenir permet à votre application d'accepter par programme les appels entrants via un appel API approprié sans aucun piratage autour du système en utilisant la réflexion ou en simulant l'utilisateur.

Comment utilisons-nous ce changement?

Vous devez vérifier la version du système au moment de l'exécution si vous prenez en charge les anciennes versions d'Android afin de pouvoir encapsuler ce nouvel appel d'API tout en maintenant la prise en charge de ces anciennes versions d'Android. Vous devez suivre la demande d'autorisations au moment de l'exécution pour obtenir cette nouvelle autorisation pendant l'exécution, comme c'est la norme sur les nouvelles versions d'Android.

Après avoir obtenu l'autorisation, votre application n'a qu'à appeler la méthode acceptRingingCall de TelecomManager . Une invocation de base ressemble alors à ceci:

TelecomManager tm = (TelecomManager) mContext
        .getSystemService(Context.TELECOM_SERVICE);

if (tm == null) {
    // whether you want to handle this is up to you really
    throw new NullPointerException("tm == null");
}

tm.acceptRingingCall();

Méthode 1: TelephonyManager.answerRingingCall ()

Pour lorsque vous avez un contrôle illimité sur l'appareil.

Qu'est-ce que c'est?

Il existe TelephonyManager.answerRingingCall () qui est une méthode interne cachée. Cela fonctionne comme un pont pour ITelephony.answerRingingCall () qui a été discuté sur les interwebs et semble prometteur au début. Il n'est pas disponible sur 4.4.2_r1 car il a été introduit uniquement dans le commit 83da75d pour Android 4.4 KitKat ( ligne 1537 sur 4.4.3_r1 ) et plus tard "réintroduit" dans le commit f1e1e77 pour Lollipop ( ligne 3138 sur 5.0.0_r1 ) en raison de la façon dont le L'arbre Git était structuré. Cela signifie qu'à moins que vous ne preniez en charge uniquement les appareils avec Lollipop, ce qui est probablement une mauvaise décision en raison de la faible part de marché de celui-ci à l'heure actuelle, vous devez toujours fournir des méthodes de secours si vous empruntez cette voie.

Comment utiliserions-nous cela?

Comme la méthode en question est masquée de l'utilisation des applications SDK, vous devez utiliser la réflexion pour examiner et utiliser dynamiquement la méthode pendant l'exécution. Si vous n'êtes pas familier avec la réflexion, vous pouvez lire rapidement Qu'est-ce que la réflexion et pourquoi est-ce utile? . Vous pouvez également approfondir les détails sur Trail: l'API Reflection si cela vous intéresse.

Et à quoi cela ressemble-t-il dans le code?

// set the logging tag constant; you probably want to change this
final String LOG_TAG = "TelephonyAnswer";

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

try {
    if (tm == null) {
        // this will be easier for debugging later on
        throw new NullPointerException("tm == null");
    }

    // do reflection magic
    tm.getClass().getMethod("answerRingingCall").invoke(tm);
} catch (Exception e) {
    // we catch it all as the following things could happen:
    // NoSuchMethodException, if the answerRingingCall() is missing
    // SecurityException, if the security manager is not happy
    // IllegalAccessException, if the method is not accessible
    // IllegalArgumentException, if the method expected other arguments
    // InvocationTargetException, if the method threw itself
    // NullPointerException, if something was a null value along the way
    // ExceptionInInitializerError, if initialization failed
    // something more crazy, if anything else breaks

    // TODO decide how to handle this state
    // you probably want to set some failure state/go to fallback
    Log.e(LOG_TAG, "Unable to use the Telephony Manager directly.", e);
}

C'est trop beau pour être vrai!

En fait, il y a un léger problème. Cette méthode doit être entièrement fonctionnelle, mais le responsable de la sécurité souhaite que les appelants maintiennent android.permission.MODIFY_PHONE_STATE . Cette autorisation est du domaine des fonctionnalités partiellement documentées du système car les tiers ne sont pas censés y toucher (comme vous pouvez le voir dans la documentation correspondante). Vous pouvez essayer d'ajouter un <uses-permission>pour cela mais cela ne servira à rien car le niveau de protection pour cette autorisation est signature | system ( voir la ligne 1201 de core / AndroidManifest sur 5.0.0_r1 ).

Vous pouvez lire le problème 34785: Mise à jour de la documentation android: protectionLevel qui a été créée en 2012 pour voir qu'il nous manque des détails sur la "syntaxe de canal" spécifique, mais après avoir expérimenté, il semble qu'il doit fonctionner comme un 'ET' signifiant tout le les indicateurs spécifiés doivent être remplis pour que l'autorisation soit accordée. En travaillant sous cette hypothèse, cela signifierait que vous devez avoir votre application:

  1. Installé en tant qu'application système.

    Cela devrait être bien et pourrait être accompli en demandant aux utilisateurs d'installer à l'aide d'un ZIP lors de la récupération, par exemple lors de l'enracinement ou de l'installation d'applications Google sur des ROM personnalisées qui ne les ont pas déjà emballées.

  2. Signé avec la même signature que les frameworks / base aka le système, aka la ROM.

    C'est là que les problèmes surgissent. Pour ce faire, vous devez avoir la main sur les clés utilisées pour la signature des frameworks / base. Vous devrez non seulement accéder aux clés de Google pour les images d'usine Nexus, mais également accéder aux clés de tous les autres OEM et développeurs de ROM. Cela ne semble pas plausible, vous pouvez donc faire signer votre application avec les clés système en créant une ROM personnalisée et en demandant à vos utilisateurs d'y basculer (ce qui peut être difficile) ou en trouvant un exploit avec lequel le niveau de protection des autorisations peut être contourné (ce qui peut aussi être difficile).

En outre, ce comportement semble être lié au problème 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS ne fonctionne plus qui utilise le même niveau de protection avec un indicateur de développement non documenté.

Travailler avec TelephonyManager semble bon, mais ne fonctionnera que si vous obtenez l'autorisation appropriée, ce qui n'est pas si facile à faire en pratique.

Qu'en est-il de l'utilisation de TelephonyManager d'une autre manière?

Malheureusement, il semble que vous deviez détenir le fichier android.permission.MODIFY_PHONE_STATE pour utiliser les outils sympas, ce qui signifie que vous aurez du mal à accéder à ces méthodes.


Méthode 2: appel de service CODE DE SERVICE

Pour quand vous pouvez tester que la construction exécutée sur l'appareil fonctionnera avec le code spécifié.

Sans pouvoir interagir avec le TelephonyManager, il y a aussi la possibilité d'interagir avec le service via l' serviceexécutable.

Comment cela marche-t-il?

C'est assez simple, mais il y a encore moins de documentation sur cette route que d'autres. Nous savons avec certitude que l'exécutable prend deux arguments - le nom du service et le code.

  • Le nom du service que nous voulons utiliser est téléphone .

    Cela peut être vu en exécutant service list.

  • Le code que nous voulons utiliser semble avoir été 6 mais semble maintenant être 5 .

    Il semble qu'il ait été basé sur IBinder.FIRST_CALL_TRANSACTION + 5 pour de nombreuses versions maintenant (de 1.5_r4 à 4.4.4_r1 ) mais lors des tests locaux, le code 5 a fonctionné pour répondre à un appel entrant. Comme Lollipo est une mise à jour massive tout autour, il est compréhensible que les internes aient également changé ici.

Cela se traduit par une commande de service call phone 5.

Comment utilisons-nous cela par programme?

Java

Le code suivant est une implémentation approximative conçue pour fonctionner comme une preuve de concept. Si vous voulez vraiment continuer et utiliser cette méthode, vous voudrez probablement consulter les directives pour une utilisation sans problème de su et éventuellement passer au libsuperuser plus développé de Chainfire .

try {
    Process proc = Runtime.getRuntime().exec("su");
    DataOutputStream os = new DataOutputStream(proc.getOutputStream());

    os.writeBytes("service call phone 5\n");
    os.flush();

    os.writeBytes("exit\n");
    os.flush();

    if (proc.waitFor() == 255) {
        // TODO handle being declined root access
        // 255 is the standard code for being declined root for SU
    }
} catch (IOException e) {
    // TODO handle I/O going wrong
    // this probably means that the device isn't rooted
} catch (InterruptedException e) {
    // don't swallow interruptions
    Thread.currentThread().interrupt();
}

Manifeste

<!-- Inform the user we want them root accesses. -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>

Cela nécessite-t-il vraiment un accès root?

Malheureusement, cela semble être le cas. Vous pouvez essayer d'utiliser Runtime.exec dessus, mais je n'ai pas pu avoir de chance avec cette route.

À quel point est-ce stable?

Je suis content que vous ayez posé la question. En raison de l'absence de documentation, cela peut se rompre entre différentes versions, comme illustré par la différence de code apparente ci-dessus. Le nom du service devrait probablement rester téléphone dans diverses versions, mais pour autant que nous sachions, la valeur du code peut changer entre plusieurs versions de la même version (modifications internes par, par exemple, le skin de l'OEM), à son tour cassant la méthode utilisée. Il convient donc de mentionner que les tests ont eu lieu sur un Nexus 4 (mako / occam). Je vous déconseille personnellement d'utiliser cette méthode, mais comme je ne suis pas en mesure de trouver une méthode plus stable, je pense que c'est la meilleure solution.


Méthode originale: intentions de code-clé du casque

Pour les moments où vous devez vous installer.

La section suivante a été fortement influencée par cette réponse par Riley C .

La méthode d'intention de casque simulée telle que publiée dans la question d'origine semble être diffusée exactement comme on pouvait s'y attendre, mais elle ne semble pas atteindre l'objectif de répondre à l'appel. Bien qu'il semble y avoir du code en place qui devrait gérer ces intentions, ils ne sont tout simplement pas pris en compte, ce qui doit signifier qu'il doit y avoir une sorte de nouvelles contre-mesures en place contre cette méthode. Le journal ne montre rien d'intéressant non plus et je ne pense pas personnellement que creuser dans la source Android pour cela vaudra la peine juste en raison de la possibilité que Google introduise un léger changement qui rompt facilement la méthode utilisée de toute façon.

Y a-t-il quelque chose que nous pouvons faire maintenant?

Le comportement peut être reproduit de manière cohérente à l'aide de l'exécutable d'entrée. Il prend un argument keycode, pour lequel nous passons simplement KeyEvent.KEYCODE_HEADSETHOOK . La méthode ne nécessite même pas d'accès root, ce qui la rend adaptée aux cas d'utilisation courants dans le grand public, mais il y a un petit inconvénient dans la méthode - l'événement de pression sur le bouton du casque ne peut pas être spécifié pour exiger une autorisation, ce qui signifie qu'il fonctionne comme un vrai appuyer sur un bouton et faire des bulles tout au long de la chaîne, ce qui signifie que vous devez être prudent quant au moment de simuler la pression sur le bouton car cela pourrait, par exemple, déclencher le lecteur de musique pour démarrer la lecture si personne d'autre de priorité plus élevée n'est prêt à gérer l'événement.

Code?

new Thread(new Runnable() {

    @Override
    public void run() {
        try {
            Runtime.getRuntime().exec("input keyevent " +
                    Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
        } catch (IOException e) {
            // Runtime.exec(String) had an I/O problem, try to fall back
            String enforcedPerm = "android.permission.CALL_PRIVILEGED";
            Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
                    Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
                            KeyEvent.KEYCODE_HEADSETHOOK));
            Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
                    Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
                            KeyEvent.KEYCODE_HEADSETHOOK));

            mContext.sendOrderedBroadcast(btnDown, enforcedPerm);
            mContext.sendOrderedBroadcast(btnUp, enforcedPerm);
        }
    }

}).start();

tl; dr

Il existe une belle API publique pour Android 8.0 Oreo et versions ultérieures.

Il n'y a pas d'API publique avant Android 8.0 Oreo. Les API internes sont interdites ou simplement sans documentation. Vous devez procéder avec prudence.

Valter Jansons
la source
En ce qui concerne les intentions de code-clé du casque, avez-vous vérifié la source Google ici pour une raison quelconque pour laquelle ils cesseraient d'être agis? Le plus drôle est que les appels peuvent toujours être facilement refusés avec ces intentions (il suffit d'émuler une pression longue), mais rien ne fonctionne pour répondre. Je n'ai pas encore trouvé de vérification d'autorisation explicite ou autre blocage potentiel et j'espère qu'un deuxième regard pourrait découvrir quelque chose.
Riley C
Était un peu occupé, d'où le retard - j'essaierai de passer un peu de temps à comprendre cela. Après un rapide coup d'œil, il semble que CallsManager construit HeadsetMediaButton. Le rappel de session doit prendre soin d'appeler handleHeadsetHook (KeyEvent) lors des rappels de MediaSessionManager. Tout le code semble correspondre ... mais je me demande, quelqu'un pourrait-il supprimer l'intention KeyEvent.ACTION_DOWN de tester? (Autrement dit, ne déclenchez KeyEvent.ACTION_UP qu'une seule fois.)
Valter Jansons
En fait, HeadsetMediaButton fonctionne avec MediaSession et n'interagit pas directement avec MediaSessionManager ...
Valter Jansons
1
Pour ceux d'entre vous qui ont réussi à trouver une situation où la méthode originale ne semble pas fonctionner (par exemple, Lollipop a des problèmes), j'ai de bonnes et de mauvaises nouvelles: j'ai réussi à faire fonctionner ACTION_UP à 100%, dans mon code, avec un FULL_WAKE_LOCK. Cela ne fonctionnera pas avec un PARTIAL_WAKE_LOCK. Il n'y a absolument aucune documentation sur le pourquoi de cela. Je détaillerai cela dans une prochaine réponse lorsque je testerai mon code d'expérience de manière plus approfondie. La mauvaise nouvelle est, bien sûr, que FULL_WAKE_LOCK est obsolète, il s'agit donc d'un correctif qui ne durera que tant que Google le conservera dans l'API.
leRobot
1
La capture de la réponse originale n'est pas appelée dans de nombreux cas. J'ai trouvé qu'il était préférable d'appeler d'abord l'exécutif, puis d'appeler le bouton de toute façon juste après.
Warpzit
36

La solution entièrement fonctionnelle est basée sur le code @Valter Strods.

Pour le faire fonctionner, vous devez afficher une activité (invisible) sur l'écran de verrouillage où le code est exécuté.

AndroidManifest.xml

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />

<activity android:name="com.mysms.android.lib.activity.AcceptCallActivity"
        android:launchMode="singleTop"
        android:excludeFromRecents="true"
        android:taskAffinity=""
        android:configChanges="orientation|keyboardHidden|screenSize"
        android:theme="@style/Mysms.Invisible">
    </activity>

Activité d'acceptation d'appel

package com.mysms.android.lib.activity;

import android.app.Activity;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import android.view.WindowManager;

import org.apache.log4j.Logger;

import java.io.IOException;

public class AcceptCallActivity extends Activity {

     private static Logger logger = Logger.getLogger(AcceptCallActivity.class);

     private static final String MANUFACTURER_HTC = "HTC";

     private KeyguardManager keyguardManager;
     private AudioManager audioManager;
     private CallStateReceiver callStateReceiver;

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);

         keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
         audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
     }

     @Override
     protected void onResume() {
         super.onResume();

         registerCallStateReceiver();
         updateWindowFlags();
         acceptCall();
     }

     @Override
     protected void onPause() {
         super.onPause();

         if (callStateReceiver != null) {
              unregisterReceiver(callStateReceiver);
              callStateReceiver = null;
         }
     }

     private void registerCallStateReceiver() {
         callStateReceiver = new CallStateReceiver();
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
         registerReceiver(callStateReceiver, intentFilter);
     }

     private void updateWindowFlags() {
         if (keyguardManager.inKeyguardRestrictedInputMode()) {
              getWindow().addFlags(
                       WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
                                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
                                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         } else {
              getWindow().clearFlags(
                       WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
                                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
                                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         }
     }

     private void acceptCall() {

         // for HTC devices we need to broadcast a connected headset
         boolean broadcastConnected = MANUFACTURER_HTC.equalsIgnoreCase(Build.MANUFACTURER)
                  && !audioManager.isWiredHeadsetOn();

         if (broadcastConnected) {
              broadcastHeadsetConnected(false);
         }

         try {
              try {
                  logger.debug("execute input keycode headset hook");
                  Runtime.getRuntime().exec("input keyevent " +
                           Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));

              } catch (IOException e) {
                  // Runtime.exec(String) had an I/O problem, try to fall back
                  logger.debug("send keycode headset hook intents");
                  String enforcedPerm = "android.permission.CALL_PRIVILEGED";
                  Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
                           Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
                                    KeyEvent.KEYCODE_HEADSETHOOK));
                  Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
                           Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
                                    KeyEvent.KEYCODE_HEADSETHOOK));

                  sendOrderedBroadcast(btnDown, enforcedPerm);
                  sendOrderedBroadcast(btnUp, enforcedPerm);
              }
         } finally {
              if (broadcastConnected) {
                  broadcastHeadsetConnected(false);
              }
         }
     }

     private void broadcastHeadsetConnected(boolean connected) {
         Intent i = new Intent(Intent.ACTION_HEADSET_PLUG);
         i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
         i.putExtra("state", connected ? 1 : 0);
         i.putExtra("name", "mysms");
         try {
              sendOrderedBroadcast(i, null);
         } catch (Exception e) {
         }
     }

     private class CallStateReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
              finish();
         }
     }
}

Style

<style name="Mysms.Invisible">
    <item name="android:windowFrame">@null</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowAnimationStyle">@null</item>
</style>

Enfin appelez la magie!

Intent intent = new Intent(context, AcceptCallActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
            | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
notz
la source
1
où mi suppoz pour ajouter le code sous "Enfin appeler la magie". Fonctionnera-t-il pour Android 6.0
Akshay Shah
Je suis venu ici pour dire que broadcastHeadsetConnected (booléen connecté) est ce qui a résolu un problème dans un appareil Samsung A3 2016. Sans cela, une méthode très similaire (utilisant une activité distincte et transparente et des threads pour les appels et autres) fonctionnait pleinement pour environ 20 appareils testés, puis cet A3 est arrivé et m'a obligé à revérifier cette question pour de nouvelles réponses. Après comparaison avec mon code, c'était la différence significative!
leRobot
1
Comment puis-je également rejeter un appel? Pouvez-vous mettre à jour la réponse pour le montrer?
Amanni
@leRobot Cette réponse vérifie s'il s'agit d'un appareil HTC à diffuserHeadsetConnected, comment pouvez-vous vérifier s'il s'agit d'un appareil Samsung A3 2016? Au fait, c'est vraiment une bonne réponse, mon application peut répondre à l'appel téléphonique même si l'écran est verrouillé.
eepty le
@eepty Vous pouvez utiliser la référence officielle de l'appareil pour les données de construction. ( support.google.com/googleplay/answer/1727131?hl=fr ). J'utilise Build.MODEL.startsWith ("SM-A310") pour l'A3 2016. MAIS! Je peux confirmer que l'A3 2016 ne prend pas en charge les diffusions connectées au casque! Ce qui a réellement résolu mon problème a été de changer l'ordre pour que le Runtime.getRuntime (). Exec (... se déclenche en premier pour ces appareils. Il semble fonctionner à chaque fois pour cet appareil et ne revient pas à l'exception.
leRobot
14

Ce qui suit est une approche alternative qui a fonctionné pour moi. Il envoie l'événement clé au serveur de télécommunications directement à l'aide de l'API MediaController. Cela nécessite que l'application dispose de l' autorisation BIND_NOTIFICATION_LISTENER_SERVICE et que l'utilisateur accorde explicitement l'accès à la notification:

@TargetApi(Build.VERSION_CODES.LOLLIPOP) 
void sendHeadsetHookLollipop() {
    MediaSessionManager mediaSessionManager =  (MediaSessionManager) getApplicationContext().getSystemService(Context.MEDIA_SESSION_SERVICE);

    try {
        List<MediaController> mediaControllerList = mediaSessionManager.getActiveSessions 
                     (new ComponentName(getApplicationContext(), NotificationReceiverService.class));

        for (MediaController m : mediaControllerList) {
             if ("com.android.server.telecom".equals(m.getPackageName())) {
                 m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
                 log.info("HEADSETHOOK sent to telecom server");
                 break;
             }
        }
    } catch (SecurityException e) {
        log.error("Permission error. Access to notification not granted to the app.");      
    }  
}

NotificationReceiverService.class dans le code ci-dessus pourrait être juste une classe vide.

import android.service.notification.NotificationListenerService;

public class NotificationReceiverService extends NotificationListenerService{
     public NotificationReceiverService() {
     }
}

Avec la section correspondante dans le manifeste:

    <service android:name=".NotificationReceiverService" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
        android:enabled="true" android:exported="true">
    <intent-filter>
         <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>

Étant donné que la cible de l'événement est explicite, cela devrait probablement éviter tout effet secondaire du déclenchement du lecteur multimédia.

Remarque: le serveur de télécommunications peut ne pas être immédiatement actif après l'événement de sonnerie. Pour que cela fonctionne de manière fiable, il peut être utile que l'application implémente MediaSessionManager.OnActiveSessionsChangedListener pour surveiller le moment où le serveur de télécommunications devient actif, avant d'envoyer l'événement.

Mise à jour:

Dans Android O , il faut simulerACTION_DOWN avant ACTION_UP, sinon ce qui précède n'a aucun effet. c'est-à-dire que ce qui suit est nécessaire:

m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));

Mais comme un appel officiel pour répondre à un appel est disponible depuis Android O (voir la réponse du haut), il se peut que ce hack ne soit plus nécessaire, à moins que l'on ne soit coincé avec un ancien niveau d'API de compilation avant Android O.

headuck
la source
Cela n'a pas fonctionné pour moi. Il a renvoyé une erreur d'autorisation. Accès à la notification non accordé à l'application. J'utilise Android L
Jame
2
Cela nécessite une étape supplémentaire consistant à accorder explicitement l'autorisation par l'utilisateur, quelque part dans le menu des paramètres en fonction du système, en plus d'accepter l'autorisation dans le manifeste.
headuck
signaler cela semble fonctionner dans un cas étroit: Galaxy A3 2016 avec Marshmallow. Je vais tester cela dans un groupe de périphériques A3 qui ne fonctionnent pas avec la méthode d'entrée keyevent en raison d'une EXCEPTION FATALE: java.lang.SecurityException: l'injection dans une autre application nécessite l'autorisation INJECT_EVENTS. Les appareils incriminés représentent environ 2% de ma base d'utilisateurs et je ne réplique pas leur exception, mais j'essaierai cette méthode pour voir s'ils parviennent à prendre l'appel. Heureusement, mon application demande déjà une notification explicite. accès à d'autres fins.
leRobot
après des tests approfondis, je suis heureux d'annoncer que les appareils A3 2016 qui échouaient avec exec "input keyevent" ont réussi à fonctionner avec la méthode MediaController # dispatchMediaButtonEvent (<hook KeryEvent>)). cela ne fonctionne évidemment qu'une fois que l'utilisateur a autorisé l'accès aux notifications explicites, vous devrez donc ajouter un écran dirigeant vers les paramètres Android pour cela, et vous devez essentiellement que l'utilisateur prenne des mesures supplémentaires pour cela, comme détaillé dans la réponse. Dans mon application, nous avons pris des mesures supplémentaires pour continuer à demander si l'utilisateur accède à cet écran mais n'ajoute pas de notification. accès
leRobot
Cela fonctionne sur Android Nougat. La solution de @notz fonctionne très bien autrement, mais se plaint "Seul le système peut envoyer un événement clé multimédia à la session de priorité globale" sur Android 7.
Peng Bai
9

Pour élaborer un tout petit peu sur la réponse de @Muzikant, et pour la modifier un peu pour qu'elle fonctionne un peu plus propre sur mon appareil, essayez input keyevent 79la constante de KeyEvent.KEYCODE_HEADSETHOOK . Très grosso modo:

    new Thread(new Runnable() {

        @Override
        public void run() {

            try {

                Runtime.getRuntime().exec( "input keyevent " + KeyEvent.KEYCODE_HEADSETHOOK );
            }
            catch (Throwable t) {

                // do something proper here.
            }
        }
    }).start();

Pardonnez les assez mauvaises conventions de codage, je ne connais pas trop bien les appels à Runtime.exec (). Notez que mon appareil n'est pas rooté et que je ne demande pas de privilèges root.

Le problème avec cette approche est qu'elle ne fonctionne que sous certaines conditions (pour moi). Autrement dit, si j'exécute le fil ci-dessus à partir d'une option de menu que l'utilisateur sélectionne pendant qu'un appel sonne, l'appel répond très bien. Si je l'exécute à partir d'un récepteur qui surveille l'état des appels entrants, il est totalement ignoré.

Ainsi, sur mon Nexus 5, cela fonctionne bien pour la réponse axée sur l'utilisateur et devrait convenir à l'objectif d'un écran d'appel personnalisé. Cela ne fonctionnera tout simplement pas pour aucune sorte d'applications de type contrôle d'appel automatisé.

Il convient également de noter toutes les mises en garde possibles, notamment le fait que cela cessera probablement de fonctionner dans une ou deux mises à jour.

Riley C
la source
input keyevent 79fonctionne bien sur Sony Xperia 5.0. Fonctionne lors d'un appel depuis une activité ou depuis un récepteur de diffusion.
nicolas
0

via les commandes adb Comment prendre un appel par adb

Gardez à l'esprit qu'Android est Linux avec une JVM massive sur le front-end. Vous pouvez télécharger une application de ligne de commande et rooter le téléphone et maintenant vous avez un ordinateur Linux classique et une ligne de commande qui fait toutes les choses normales. Exécutez des scripts, vous pouvez même y ssh (astuce OpenVPN)

user1544207
la source
0

Merci @notz la réponse de travaille pour moi sur le Lolillop. Pour que ce code fonctionne avec l'ancien SDK Android, vous pouvez faire ce code:

if (Build.VERSION.SDK_INT >= 21) {  
    Intent answerCalintent = new Intent(context, AcceptCallActivity.class);  
    answerCalintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 
                             Intent.FLAG_ACTIVITY_CLEAR_TASK  | 
                             Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    context.startActivity(answerCalintent);  
}  
else {  
  if (telephonyService != null) {  
    try {  
        telephonyService.answerRingingCall();  
    }  
    catch (Exception e) {  
        answerPhoneHeadsethook();  
    }  
  }  
}  
Khac Quyet Dang
la source
0

Comment activer le téléphone à haut-parleur après avoir répondu automatiquement aux appels.

J'ai résolu mon problème ci-dessus avec setSpeakerphoneOn. Je pense que cela vaut la peine d'être publié ici, car le cas d'utilisation de la réponse automatique à un appel téléphonique nécessiterait souvent également un haut-parleur pour être utile. Merci encore à tout le monde sur ce fil, quel travail formidable.

Cela fonctionne pour moi sur Android 5.1.1 sur mon Nexus 4 sans ROOT. ;)

Autorisation requise:

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

Code Java:

// this means the phone has answered
if(state==TelephonyManager.CALL_STATE_OFFHOOK)
{
    // try and turn on speaker phone
    final Handler mHandler = new Handler();
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            AudioManager audioManager = (AudioManager) localContext.getSystemService(Context.AUDIO_SERVICE);

            // this doesnt work without android.permission.MODIFY_PHONE_STATE
            // audioManager.setMode(AudioManager.MODE_IN_CALL);

            // weirdly this works
            audioManager.setMode(AudioManager.MODE_NORMAL); // this is important
            audioManager.setSpeakerphoneOn(true);

            // note the phone interface won't show speaker phone is enabled
            // but the phone speaker will be on
            // remember to turn it back off when your done ;)
        }
    }, 500); // half a second delay is important or it might fail
}
Madhava Jay
la source
1
Intéressant. J'essaie en fait de répondre à l'appel et d'allumer le haut-parleur ensemble, donc cette approche semble résoudre les deux :). J'ai une question similaire à celle de certains commentaires dans d'autres réponses: où va ce code?
fangmobile
-1

Exécutez la commande suivante en tant que root:

input keyevent 5

Plus de détails sur la simulation d'événements clés ici .

Vous pouvez utiliser cette classe de base que j'ai créée pour exécuter des commandes en tant que root depuis votre application.

Muzikant
la source
1
Lors des tests avec un profil d'utilisateur régulier, cela a fait apparaître l'interface utilisateur en appel pour moi, me demandant de faire glisser votre doigt vers la gauche / droite pour refuser / répondre ou utiliser une action / réponse rapide. Si l'OP crée un écran d'appel entrant personnalisé , cela n'aide pas vraiment à moins qu'il ne se comporte différemment sous root, ce qui, je doute, comme s'il ne se comportait pas bien pour un utilisateur régulier, l'appel échouerait probablement simplement et non déclencher une action différente.
Valter Jansons
-2

testez ceci: ajoutez d'abord les permissions puis utilisez killCall () pour raccrocher utilisez answerCall () pour répondre à l'appel

<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"></uses-permission>


public void killCall() {
    try {
        TelephonyManager telephonyManager =
                (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);

        Class classTelephony = Class.forName(telephonyManager.getClass().getName());
        Method methodGetITelephony = classTelephony.getDeclaredMethod("getITelephony");

        methodGetITelephony.setAccessible(true);

        Object telephonyInterface = methodGetITelephony.invoke(telephonyManager);

        Class telephonyInterfaceClass =
                Class.forName(telephonyInterface.getClass().getName());
        Method methodEndCall = telephonyInterfaceClass.getDeclaredMethod("endCall");

        methodEndCall.invoke(telephonyInterface);

    } catch (Exception ex) {
        Log.d(TAG, "PhoneStateReceiver **" + ex.toString());
    }
}

public void answerCall() {
    try {
        Runtime.getRuntime().exec("input keyevent " +
                Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));

    } catch (IOException e) {
        answerRingingCallWithIntent();
    }
}

public void answerRingingCallWithIntent() {
    try {
        Intent localIntent1 = new Intent(Intent.ACTION_HEADSET_PLUG);
        localIntent1.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
        localIntent1.putExtra("state", 1);
        localIntent1.putExtra("microphone", 1);
        localIntent1.putExtra("name", "Headset");
        getContext().sendOrderedBroadcast(localIntent1, "android.permission.CALL_PRIVILEGED");

        Intent localIntent2 = new Intent(Intent.ACTION_MEDIA_BUTTON);
        KeyEvent localKeyEvent1 = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK);
        localIntent2.putExtra(Intent.EXTRA_KEY_EVENT, localKeyEvent1);
        getContext().sendOrderedBroadcast(localIntent2, "android.permission.CALL_PRIVILEGED");

        Intent localIntent3 = new Intent(Intent.ACTION_MEDIA_BUTTON);
        KeyEvent localKeyEvent2 = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK);
        localIntent3.putExtra(Intent.EXTRA_KEY_EVENT, localKeyEvent2);
        getContext().sendOrderedBroadcast(localIntent3, "android.permission.CALL_PRIVILEGED");

        Intent localIntent4 = new Intent(Intent.ACTION_HEADSET_PLUG);
        localIntent4.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
        localIntent4.putExtra("state", 0);
        localIntent4.putExtra("microphone", 1);
        localIntent4.putExtra("name", "Headset");
        getContext().sendOrderedBroadcast(localIntent4, "android.permission.CALL_PRIVILEGED");
    } catch (Exception e2) {
        e2.printStackTrace();
    }
}
Michael
la source
-2

Pour info, si vous souhaitez mettre fin à un appel en cours sur Android O, Valter Method 1: TelephonyManager.answerRingingCall()fonctionne si vous modifiez la méthode que vous invoquez pour êtreendCall .

Il ne nécessite que la android.permission.CALL_PHONEpermission.

Voici le code:

// set the logging tag constant; you probably want to change this
final String LOG_TAG = "TelephonyAnswer";

public void endCall() {
    TelephonyManager tm = (TelephonyManager) mContext
            .getSystemService(Context.TELEPHONY_SERVICE);

    try {
        if (tm == null) {
            // this will be easier for debugging later on
            throw new NullPointerException("tm == null");
        }

        // do reflection magic
        tm.getClass().getMethod("endCall").invoke(tm);
    } catch (Exception e) {
        // we catch it all as the following things could happen:
        // NoSuchMethodException, if the answerRingingCall() is missing
        // SecurityException, if the security manager is not happy
        // IllegalAccessException, if the method is not accessible
        // IllegalArgumentException, if the method expected other arguments
        // InvocationTargetException, if the method threw itself
        // NullPointerException, if something was a null value along the way
        // ExceptionInInitializerError, if initialization failed
        // something more crazy, if anything else breaks

        // TODO decide how to handle this state
        // you probably want to set some failure state/go to fallback
        Log.e(LOG_TAG, "Unable to use the Telephony Manager directly.", e);
    }
}
François Dermu
la source