Como as chamadas recebidas podem ser atendidas programaticamente no Android 5.0 (Lollipop)?


87

Como estou tentando criar uma tela personalizada para chamadas recebidas, estou tentando atender programaticamente uma chamada recebida. Estou usando o código a seguir, mas não está funcionando no 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");

oh cara, por que correr para isso, apenas deslize cara! parece mais fácil para mim \ m /
nobalG

Estou criando uma tela de chamada recebida personalizada para usuários do Android.
maveroid

2
Alguém? Eu também estou interessado neste! Tentei muitas coisas, mas não funcionaram: /
Arthur

1
@nobalG ele está dizendo programaticamente
dsharew

1
@maveroid, você encontrou uma solução alternativa para o Android 5.0?
arthursfreire

Respostas:


155

Atualização com Android 8.0 Oreo

Embora a pergunta tenha sido originalmente feita para o suporte do Android L, as pessoas ainda parecem estar acertando essa pergunta e resposta, então vale a pena descrever as melhorias introduzidas no Android 8.0 Oreo. Os métodos compatíveis com versões anteriores ainda são descritos abaixo.

O que mudou?

A partir do Android 8.0 Oreo , o grupo de permissão PHONE também contém a permissão ANSWER_PHONE_CALLS . Como o nome da permissão sugere, segurá-la permite que seu aplicativo aceite programaticamente chamadas de entrada por meio de uma chamada de API adequada sem qualquer invasão do sistema usando reflexão ou simulação do usuário.

Como utilizamos essa mudança?

Você deve verificar a versão do sistema no tempo de execução se estiver oferecendo suporte a versões mais antigas do Android, para que possa encapsular essa nova chamada de API enquanto mantém o suporte para essas versões mais antigas do Android. Você deve solicitar permissões em tempo de execução para obter essa nova permissão durante o tempo de execução, como é padrão nas versões mais recentes do Android.

Depois de obter a permissão, seu aplicativo apenas precisa chamar o método acceptRingingCall do TelecomManager . Uma invocação básica tem a seguinte aparência:

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étodo 1: TelephonyManager.answerRingingCall ()

Para quando você tiver controle ilimitado sobre o dispositivo.

O que é isso?

Existe TelephonyManager.answerRingingCall () que é um método interno oculto. Ele funciona como uma ponte para ITelephony.answerRingingCall () que foi discutido nas interwebs e parece promissor no início. É não disponível em 4.4.2_r1 como foi introduzida apenas em cometer 83da75d para Android 4.4 KitKat ( linha 1537 em 4.4.3_r1 ) e mais tarde "reintroduzido" em cometer f1e1e77 para Lollipop ( linha 3138 em 5.0.0_r1 ) devido à forma como o A árvore Git foi estruturada. Isso significa que, a menos que você ofereça suporte apenas a dispositivos com Lollipop, o que provavelmente é uma decisão ruim com base na pequena participação de mercado dele no momento, você ainda precisará fornecer métodos de fallback se for seguir esse caminho.

Como usaríamos isso?

Como o método em questão está oculto do uso de aplicativos SDK, você precisa usar reflexão para examinar e usar o método dinamicamente durante o tempo de execução. Se você não está familiarizado com a reflexão, pode ler rapidamente O que é reflexão e por que ela é útil? . Você também pode se aprofundar nos detalhes em Trail: The Reflection API se estiver interessado em fazê-lo.

E como isso fica no código?

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

Isso é bom demais para ser verdade!

Na verdade, há um pequeno problema. Este método deve ser totalmente funcional, mas o gerenciador de segurança deseja que os chamadores mantenham android.permission.MODIFY_PHONE_STATE . Essa permissão está no reino de recursos apenas parcialmente documentados do sistema, já que terceiros não devem tocá-la (como você pode ver na documentação). Você pode tentar adicionar um <uses-permission>para ele, mas não adiantará porque o nível de proteção para essa permissão é assinatura | sistema ( consulte a linha 1201 do core / AndroidManifest em 5.0.0_r1 ).

Você pode ler o Problema 34785: Atualizar a documentação do android: protectionLevel, criada em 2012, para ver que faltam detalhes sobre a "sintaxe de tubo" específica, mas, a partir de experimentos, parece que deve funcionar como um 'AND' significando todos sinalizadores especificados devem ser cumpridos para que a permissão seja concedida. Trabalhando sob essa suposição, isso significaria que você deve ter seu aplicativo:

  1. Instalado como um aplicativo do sistema.

    Isso deve ser adequado e pode ser realizado solicitando aos usuários que instalem usando um ZIP na recuperação, como ao fazer o root ou instalar aplicativos do Google em ROMs personalizados que ainda não os têm embalados.

  2. Assinado com a mesma assinatura que frameworks / base, também conhecido como sistema, também conhecido como ROM.

    É aqui que surgem os problemas. Para fazer isso, você precisa ter em mãos as chaves usadas para assinar frameworks / base. Você não só teria que obter acesso às chaves do Google para imagens de fábrica do Nexus, mas também teria acesso às chaves de todos os outros OEMs e desenvolvedores de ROM. Isso não parece plausível, então você pode ter seu aplicativo assinado com as chaves do sistema criando uma ROM personalizada e pedindo que seus usuários mudem para ela (o que pode ser difícil) ou encontrando um exploit com o qual o nível de proteção de permissão possa ser contornado (o que também pode ser difícil).

Além disso, esse comportamento parece estar relacionado ao problema 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS não funciona mais, o que também utiliza o mesmo nível de proteção junto com um sinalizador de desenvolvimento não documentado.

Trabalhar com o TelephonyManager parece bom, mas não funcionará a menos que você obtenha a permissão apropriada, o que não é tão fácil de fazer na prática.

Que tal usar o TelephonyManager de outras maneiras?

Infelizmente, parece que você deve manter o android.permission.MODIFY_PHONE_STATE para usar as ferramentas legais, o que significa que você terá dificuldade em obter acesso a esses métodos.


Método 2: chamada de serviço CÓDIGO DE SERVIÇO

Para quando você puder testar se a compilação em execução no dispositivo funcionará com o código especificado.

Sem poder interagir com o TelephonyManager, também existe a possibilidade de interagir com o serviço através do serviceexecutável.

Como é que isso funciona?

É bastante simples, mas há ainda menos documentação sobre essa rota do que outras. Sabemos com certeza que o executável recebe dois argumentos - o nome do serviço e o código.

  • O nome do serviço que queremos usar é telefone .

    Isso pode ser visto executando service list.

  • O código que queremos usar parece ter sido 6, mas agora parece ser 5 .

    Parece que foi baseado em IBinder.FIRST_CALL_TRANSACTION + 5 para muitas versões agora (de 1.5_r4 a 4.4.4_r1 ), mas durante o teste local, o código 5 funcionou para atender uma chamada recebida. Como o Lollipo é uma atualização massiva em todos os sentidos, é compreensível que as partes internas tenham mudado aqui também.

Isso resulta com um comando de service call phone 5.

Como usamos isso programaticamente?

Java

O código a seguir é uma implementação aproximada feita para funcionar como uma prova de conceito. Se você realmente deseja seguir em frente e usar este método, provavelmente deseja verificar as diretrizes para o uso do su sem problemas e, possivelmente, alternar para o libsuperuser mais desenvolvido da 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();
}

Manifesto

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

Isso realmente requer acesso root?

Infelizmente, parece que sim. Você pode tentar usar Runtime.exec nele, mas não consegui obter nenhuma sorte com essa rota.

Quão estável é isso?

Estou feliz que você perguntou. Por não estar documentado, isso pode falhar em várias versões, conforme ilustrado pela aparente diferença de código acima. O nome do serviço provavelmente deve permanecer phone em vários builds, mas pelo que sabemos, o valor do código pode mudar em vários builds da mesma versão (modificações internas por, digamos, o skin do OEM), por sua vez, quebrando o método usado. Portanto, vale a pena mencionar que o teste ocorreu em um Nexus 4 (mako / occam). Eu pessoalmente aconselharia você a não usar esse método, mas como não consigo encontrar um método mais estável, acredito que essa seja a melhor chance.


Método original: intenções do código-chave do fone de ouvido

Por momentos em que você tem que se acomodar.

A secção seguinte foi fortemente influenciado por esta resposta por Riley C .

O método de intenção de fone de ouvido simulado, conforme postado na pergunta original, parece ser transmitido exatamente como seria de se esperar, mas não parece cumprir o objetivo de atender a chamada. Embora pareça haver um código que deve lidar com essas intents, elas simplesmente não estão sendo consideradas, o que significa que deve haver algum tipo de contra-medidas contra esse método. O log também não mostra nada de interesse e eu pessoalmente não acredito que vá valer a pena vasculhar a fonte do Android para isso apenas devido à possibilidade de o Google introduzir uma pequena mudança que facilmente quebra o método usado de qualquer maneira.

Existe algo que possamos fazer agora?

O comportamento pode ser reproduzido de forma consistente usando o executável de entrada. Ele recebe um argumento de código-chave, para o qual simplesmente passamos KeyEvent.KEYCODE_HEADSETHOOK . O método nem mesmo requer acesso root, tornando-o adequado para casos de uso comuns no público em geral, mas há uma pequena desvantagem no método - o evento de pressionamento de botão do fone de ouvido não pode ser especificado para exigir uma permissão, o que significa que funciona como um verdadeiro pressione o botão e borbulha por toda a cadeia, o que significa que você deve ser cauteloso ao simular o pressionamento do botão, pois isso poderia, por exemplo, acionar o reprodutor de música para iniciar a reprodução se ninguém mais com maior prioridade estiver pronto para lidar o evento.

Código?

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

Há uma boa API pública para Android 8.0 Oreo e posterior.

Não há API pública anterior ao Android 8.0 Oreo. As APIs internas estão fora dos limites ou simplesmente sem documentação. Você deve proceder com cautela.


Com relação às intenções do código do fone de ouvido, você verificou a fonte do Google aqui por algum motivo pelo qual eles parariam de receber ações? O engraçado é que as chamadas ainda podem ser facilmente recusadas com essas intents (apenas emule um toque longo), mas nada funciona para responder. Ainda estou para encontrar uma verificação de permissão explícita ou outro bloqueio potencial e estou esperando que um segundo par de olhos possa descobrir algo.
Riley C

Estava um pouco ocupado, por isso o atraso - vou tentar gastar algum tempo tentando descobrir isso. Após uma rápida olhada, parece que o CallsManager constrói o HeadsetMediaButton. O retorno de chamada da sessão deve ter o cuidado de chamar handleHeadsetHook (KeyEvent) em retornos de chamada de MediaSessionManager. Todo o código parece corresponder ... mas eu me pergunto, alguém poderia remover a intenção KeyEvent.ACTION_DOWN para testar? (Isto é, só dispare o KeyEvent.ACTION_UP uma única vez.)
Valter Jansons

Na verdade, HeadsetMediaButton trabalha com o MediaSession e não interage diretamente com o MediaSessionManager ...
Valter Jansons

1
Para aqueles de vocês que conseguiram encontrar uma situação em que o Método Original não parece funcionar (por exemplo, o Lollipop tem problemas), tenho boas e más notícias: consegui fazer o ACTION_UP funcionar 100%, no meu código, com um FULL_WAKE_LOCK. Não funcionará com PARTIAL_WAKE_LOCK. Não há absolutamente nenhuma documentação sobre o porquê disso. Detalharei isso em uma resposta futura, quando testarei meu código de experimento mais extensivamente. A má notícia, claro, é que FULL_WAKE_LOCK está obsoleto, então esta é uma correção que durará apenas enquanto o Google a mantiver na API.
leRobot

1
A captura da resposta original não é chamada em muitos casos. Achei melhor apenas ligar para exec primeiro e, em seguida, abrir o botão de qualquer maneira logo depois.
Warpzit de

36

A solução totalmente funcional é baseada no código @Valter Strods.

Para fazê-lo funcionar, você deve exibir uma atividade (invisível) na tela de bloqueio onde o código é executado.

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>

Atividade de aceitação de chamada

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

Estilo

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

Finalmente chame a magia!

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

1
onde mi suppoz para adicionar o código em "Finalmente chame a magia". Funcionará para Android 6.0
Akshay Shah

Vim aqui para dizer que broadcastHeadsetConnected (boolean conectado) é o que resolveu um problema em um aparelho Samsung A3 2016. Sem isso, um método muito semelhante (usando atividade separada e transparente e threads para invocações e outros enfeites) estava funcionando totalmente para cerca de 20 dispositivos testados, então esse A3 apareceu e me forçou a verificar novamente esta questão para novas respostas. Depois de comparar com meu código, essa foi a diferença significativa!
leRobot

1
Como posso rejeitar uma chamada também? Você pode atualizar a resposta para mostrar isso?
Amanni

@leRobot Esta resposta verifica se é um dispositivo HTC para broadcastHeadsetConnected, como você pode verificar se é um dispositivo Samsung A3 2016? A propósito, esta é realmente uma boa resposta, meu aplicativo pode atender a ligação mesmo com a tela bloqueada.
eepty

@eepty Você pode usar a referência oficial do dispositivo para dados do Build. ( support.google.com/googleplay/answer/1727131?hl=en ). Eu uso Build.MODEL.startsWith ("SM-A310") para o A3 2016. MAS! Posso confirmar que o A3 2016 não oferece suporte a transmissões com fone de ouvido conectado! O que realmente resolveu meu problema foi alterar a ordem para que Runtime.getRuntime (). Exec (... seja acionado primeiro para esses dispositivos. Parece funcionar sempre para esse dispositivo e não retorna à exceção.
leRobot

14

A seguir está uma abordagem alternativa que funcionou para mim. Ele envia o evento-chave para o servidor de telecomunicações diretamente usando a API MediaController. Isso requer que o aplicativo tenha permissão BIND_NOTIFICATION_LISTENER_SERVICE e receba a concessão explícita de acesso à Notificação do usuário:

@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 no código acima pode ser apenas uma classe vazia.

import android.service.notification.NotificationListenerService;

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

Com a seção correspondente no manifesto:

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

Como o alvo do evento é explícito, isso provavelmente deve evitar qualquer efeito colateral de acionar o reprodutor de mídia.

Nota: o servidor de telecomunicações pode não estar imediatamente ativo após o evento de toque. Para que isso funcione de forma confiável, pode ser útil para o aplicativo implementar MediaSessionManager.OnActiveSessionsChangedListener para monitorar quando o servidor de telecomunicações se torna ativo, antes de enviar o evento.

Atualizar:

No Android O , é necessário simular ACTION_DOWNantes ACTION_UP, caso contrário, o acima não terá efeito. ou seja, o seguinte é necessário:

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

Mas, uma vez que uma chamada oficial para atender chamadas está disponível desde o Android O (veja a resposta superior), pode não haver mais necessidade desse hack, a menos que alguém esteja preso a um nível de API de compilação antigo antes do Android O.


Não funcionou para mim. Ele retornou um erro de permissão. Acesso à notificação não concedido ao aplicativo. Estou usando o Android L
Jame

2
Isso requer uma etapa adicional de conceder explicitamente a permissão pelo usuário, em algum lugar no menu de configurações dependendo do sistema, além de aceitar a permissão no manifesto.
headuck

relatar isso parece funcionar em um caso estreito: Galaxy A3 2016 com Marshmallow. Estarei testando isso em um grupo de dispositivos A3 que não estão funcionando com o método keyevent de entrada devido a uma EXCEÇÃO FATAL: java.lang.SecurityException: injetar em outro aplicativo requer permissão INJECT_EVENTS. Dispositivos infratores são cerca de 2% da minha base de usuários e não estou replicando sua exceção, mas tentarei este método para ver se eles conseguem atender a chamada. Felizmente, meu aplicativo já está solicitando notificação explícita. acesso para outros fins.
leRobot

após testes extensivos, estou feliz em relatar que os dispositivos A3 2016 que estavam falhando com exec "keyevent" conseguiram funcionar com o método MediaController # dispatchMediaButtonEvent (<hook KeryEvent>)). isso obviamente só funciona depois que o usuário permite acesso explícito à notificação, então você terá que adicionar uma tela direcionando para as configurações do Android para isso, e você basicamente precisa que o usuário execute uma ação extra para isso, conforme detalhado na resposta. Em meu aplicativo, tomamos medidas extras para continuar perguntando se o usuário vai para aquela tela, mas não adiciona notif. acesso
leRobot

Isso funciona no Android Nougat. A solução de @notz funciona muito bem de outra forma, mas reclama "Apenas o sistema pode enviar evento de chave de mídia para a sessão de prioridade global" no Android 7.
Peng Bai

9

Para elaborar um pouco sobre a resposta de @Muzikant, e modificá-la um pouco para funcionar um pouco mais limpa no meu dispositivo, tente input keyevent 79a constante para KeyEvent.KEYCODE_HEADSETHOOK . Muito aproximadamente:

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

Perdoe as convenções de codificação bastante ruins, não sou muito versado em chamadas Runtime.exec (). Observe que meu dispositivo não tem acesso root, nem estou solicitando privilégios de root.

O problema com essa abordagem é que ela só funciona sob certas condições (para mim). Ou seja, se eu executar o tópico acima a partir de uma opção de menu que o usuário seleciona enquanto uma chamada está tocando, a chamada atende perfeitamente. Se eu executá-lo de um receptor que monitora o status das chamadas recebidas, ele é totalmente ignorado.

Portanto, no meu Nexus 5, ele funciona bem para atender o usuário e deve se adequar ao propósito de uma tela de chamada personalizada. Ele simplesmente não funcionará para nenhum tipo de aplicativo automatizado de controle de chamadas.

Também digno de nota são todas as advertências possíveis, incluindo que isso também irá provavelmente parar de funcionar em uma atualização ou duas.


input keyevent 79funciona bem no Sony Xperia 5.0. Funciona ao chamar de uma atividade ou de um receptor de transmissão.
Nicolas

0

por meio dos comandos adb Como atender uma chamada por adb

Lembre-se de que o Android é Linux com uma grande JVM no front end. Você pode baixar um aplicativo de linha de comando e enraizar o telefone e agora você tem um computador Linux normal e linha de comando que faz todas as coisas normais. Execute scripts, você pode até usar ssh para ele (truque do OpenVPN)


0

Obrigado @notz, a resposta de está funcionando para mim no Lolillop. Para manter esse código funcionando com o SDK do Android antigo, você pode fazer este código:

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

0

Como ligar o viva-voz após atender chamadas automaticamente.

Resolvi meu problema acima com setSpeakerphoneOn. Acho que vale a pena postar aqui, já que o caso de uso para atendimento automático de uma chamada telefônica muitas vezes também requer o viva-voz para ser útil. Obrigado novamente a todos neste tópico, que trabalho incrível.

Isso funciona para mim no Android 5.1.1 no meu Nexus 4 sem ROOT. ;)

Permissão necessária:

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

Código 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
}

1
Interessante. Na verdade, estou tentando atender a chamada e ligar o viva-voz juntos, então essa abordagem parece resolver os dois :). Tenho uma pergunta semelhante a alguns dos comentários em outras respostas: para onde vai esse código?
fangmobile

-1

Execute o seguinte comando como root:

input keyevent 5

Mais detalhes sobre a simulação de eventos-chave aqui .

Você pode usar esta classe base que criei para executar comandos como root do seu aplicativo.


1
Durante o teste com um perfil de usuário normal, isso trouxe a IU em chamada para mim, pedindo-me para deslizar para a esquerda / direita para recusar / responder ou usar uma ação / resposta rápida. Se o OP estiver criando uma tela de chamada recebida personalizada , isso não ajudará muito, a menos que se comporte de maneira diferente no root, o que duvido que não tenha se comportado bem para um usuário comum, a chamada provavelmente falharia e não desencadear uma ação diferente.
Valter Jansons

-2

teste isto: primeiro adicione as permissões e use killCall () para desligar use answerCall () para atender a chamada

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

-2

Para sua informação, se você estiver interessado em como ENCERRAR uma chamada em andamento no Android O, Valter Method 1: TelephonyManager.answerRingingCall()funciona se você alterar o método que você invocar para ser endCall.

Requer apenas a android.permission.CALL_PHONEpermissão.

Aqui está o código:

// 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);
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.