Как можно ответить на входящие вызовы программно в Android 5.0 (Lollipop)?


Как я пытаюсь создать пользовательский экран для входящих звонков я пытаюсь программно ответить на входящий вызов. Я использую следующий код, но он не работает в 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");
10 78

10 ответов:

обновление с Android 8.0 Oreo

несмотря на то, что вопрос был первоначально задан для поддержки Android L, люди все еще, похоже, задают этот вопрос и отвечают, поэтому стоит описать улучшения, введенные в Android 8.0 Oreo. Методы обратной совместимости по-прежнему описаны ниже.

что изменилось?

начиная с Android 8.0 Oreo на телефон группа разрешение также содержит элемент ANSWER_PHONE_CALLS разрешение. Как следует из названия разрешения, удержание его позволяет вашему приложению программно принимать входящие вызовы через правильный вызов API без взлома системы с помощью отражения или имитации пользователя.

как мы используем эти изменения?

вы должны проверяем версию системы во время выполнения если вы поддерживаете более старые версии Android, так что вы можете инкапсулировать этот новый API вызов при сохранении поддержки для этих старых версий Android. Вы должны следовать запрос разрешений во время выполнения чтобы получить это новое разрешение во время выполнения, как это обычно делается в новых версиях Android.

после получения разрешения, ваше приложение должно просто вызвать TelecomManager это acceptRingingCall метод. Основной вызов выглядит следующим образом:

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

Способ 1: TelephonyManager.answerRingingCall ()

когда у вас есть неограниченный контроль над устройством.

что это?

есть TelephonyManager.answerRingingCall (), который является скрытым, внутренним методом. Он работает как мост для ITelephony.answerRingingCall (), который был обсужден на interwebs и кажется многообещающим в начале. Это не доступный на 4.4. 2_r1 как это было введено только в commit 83da75d для Android 4.4 KitKat (строка 1537 на 4.4. 3_r1) и позже "повторно введен" в commit f1e1e77 для Lollipop (строка 3138 на 5.0. 0_r1) из-за того, как было структурировано дерево Git. Это означает, что если вы не поддерживаете только устройства с Lollipop, что, вероятно, является плохим решением, основанным на крошечной доле рынка на данный момент, вам все равно нужно предоставить резервные методы, Если вы идете по этому маршруту.

как бы мы использовать это?

поскольку рассматриваемый метод скрыт от использования приложений SDK, вам нужно использовать отражение для динамического изучения и использования метода во время выполнения. Если вы не знакомы с отражением, вы можете быстро читать что такое рефлексия, и почему она полезна?. Вы также можете углубиться в специфику на Trail: The Reflection API если вы заинтересованы в этом.

и как это выглядит в код?

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

это слишком хорошо, чтобы быть правдой!

на самом деле, есть одна небольшая проблема. Этот метод должен быть полностью функциональным, но менеджер безопасности хочет, чтобы вызывающие абоненты удерживали android.разрешение.MODIFY_PHONE_STATE. Это разрешение относится к области только частично документированных функций системы, поскольку третьи стороны не должны прикасаться к ней (как вы можете видеть из документации для нее). Вы можете попробовать добавить <uses-permission> для его но это не поможет, потому что уровень защиты для этого разрешения подпись|системы (см. строку 1201 core / AndroidManifest на 5.0. 0_r1).

вы можете узнать выпуск 34785: обновление android: документация уровня защиты который был создан еще в 2012 году, чтобы увидеть, что нам не хватает подробностей о конкретном "синтаксисе канала", но из экспериментов вокруг, похоже, он должен функционировать как "и", что означает, что все указанные флаги имеют должно быть выполнено для получения разрешения. Работая в этом предположении, это будет означать, что вы должны иметь свое приложение:

  1. установить как системное приложение.

    это должно быть нормально и может быть достигнуто путем запроса пользователей для установки с помощью ZIP в восстановлении, например, при укоренении или установке приложений Google на пользовательских ПЗУ, которые не имеют их уже упакованы.

  2. подпись с той же сигнатурой, что и фреймворки/база, ака система, ака ПЗУ.

    вот где появляются проблемы. Для этого вам нужно иметь свои руки на ключах, используемых для подписания фреймворков/базы. Вам нужно будет не только получить доступ к ключам Google для изображений Nexus factory, но и получить доступ ко всем другим ключам разработчиков OEMs и ROM. Это не кажется правдоподобным, поэтому вы можете подписать свое приложение с помощью системных ключей, либо сделав пользовательское ПЗУ и просит ваших пользователей переключиться на него (что может быть сложно) или найти эксплойт, с помощью которого можно обойти уровень защиты разрешений (что также может быть сложно).

кроме того, это поведение, по-видимому, связано с выпуск 34792: Android Jelly Bean / 4.1: android.разрешение.Я просто больше не работает который использует тот же уровень защиты вместе с недокументированным флагом разработки, что и что ж.

работа с TelephonyManager звучит хорошо, но не будет работать, если вы не получите соответствующее разрешение, которое не так просто сделать на практике.

как насчет использования TelephonyManager другими способами?

к сожалению, это, кажется, требует от вас, чтобы держать android.разрешение.MODIFY_PHONE_STATE чтобы использовать классные инструменты, что в свою очередь означает, что вам будет трудно получить доступ к ним методы.


Способ 2: сервисный код вызова службы

когда вы можете проверить, что сборка, запущенная на устройстве, будет работать с указанным кодом.

не имея возможности взаимодействовать с TelephonyManager, есть также возможность взаимодействия с сервисом через service исполняемый файл.

как это работает?

это довольно просто, но есть еще меньше документации о этот маршрут, чем другие. Мы точно знаем, что исполняемый файл принимает два аргумента-имя службы и код.

  • The службы мы хотим использовать телефон.

    это можно увидеть, запустив service list.

  • The код мы хотим использовать, кажется, был 6 но, кажется, теперь 5.

    похоже, что это было на основе IBinder.FIRST_CALL_TRANSACTION + 5 для многих версий теперь (с 1. 5_r4 до 4.4.4_r1), но в ходе локального тестирования код 5 работал, чтобы ответить на входящий вызов. Поскольку Lollipo-это массовое обновление вокруг, понятно, что здесь также изменились внутренние компоненты.

это приводит к команде service call phone 5.

как мы используем этот программно?

Java

следующий код является грубой реализацией, выполненной для работы в качестве доказательства концепции. Если вы действительно хотите пойти дальше и использовать этот метод, вы, вероятно, хотите проверить руководящие принципы для задачи-свободного использования Су и, возможно, переключиться на более полное развитие libsuperuser by и.

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

Манифест

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

это действительно требуется корневой доступ?

к сожалению, это так. Вы можете попробовать использовать во время выполнения.старпома на нем, но я не смог получить удачу с этим маршрутом.

насколько это стабильно?

Я рад, что вы спросили. Из-за того, что это не документировано, это может разбиться на различные версии, как показано на кажущейся разнице кода выше. Имя службы, вероятно, должно остаться телефон через различные сборки, но для всех мы знаем, значение кода может изменяться в нескольких сборках одной и той же версии (внутренние модификации, например, кожа OEM), в свою очередь, нарушая используемый метод. Поэтому стоит упомянуть, что тестирование проходило на Nexus 4 (mako/occam). Я бы лично посоветовал вам не использовать этот метод, но поскольку я не могу найти более стабильный метод, я считаю, что это лучший выстрел.


оригинальный метод: гарнитура keycode намерения

для времен, когда вам нужно селиться.

на следующий раздел сильно повлияло ответ by Райли C.

имитированный метод намерения гарнитуры, опубликованный в исходном вопросе, похоже, транслируется так же, как и следовало ожидать, но он, похоже, не выполняет цель ответа на вызов. Хотя, похоже, есть код, который должен обрабатывать эти намерения, о них просто не заботятся, что должно значит, должны быть какие-то новые контрмеры против этого метода. Журнал также не показывает ничего интересного, и я лично не считаю, что копание в источнике Android для этого будет стоить только из-за возможности введения Google небольшого изменения, которое легко нарушает используемый метод в любом случае.

мы можем что-нибудь сделать прямо сейчас?

поведение может быть последовательно воспроизведено с помощью входного исполняемого файла. Она принимает в аргумент ключ, для которого мы просто передаем в KeyEvent.KEYCODE_HEADSETHOOK. Метод даже не требует корневого доступа, что делает его подходящим для распространенных случаев использования в широкой публике, но есть небольшой недостаток в методе-событие нажатия кнопки гарнитуры не может быть указано, чтобы потребовать разрешения, то есть оно работает как реальное нажатие кнопки и пузыри вверх по всей цепочке, что в свою очередь означает, что вы должны быть осторожны, когда имитировать нажатие кнопки, как это делается может, например, вызвать музыкальный проигрыватель для начала воспроизведения, если никто другой с более высоким приоритетом не готов обрабатывать событие.

код?

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

есть хороший публичный API для Android 8.0 Oreo и более поздних версий.

нет публичного API до Android 8.0 Oreo. Внутренние API-интерфейсы недоступны или просто без документации. Вы должны действовать с осторожностью.

полностью рабочее решение основано на коде @ Valter Strods.

чтобы заставить его работать, вы должны отобразить (невидимую) активность на экране блокировки, где выполняется код.

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>

Call Accept Activity

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

наконец-то вызовите магию!

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

следующий альтернативный подход, который работал для меня. Он отправляет ключевое событие на телекоммуникационный сервер непосредственно с помощью API MediaController. Для этого требуется, чтобы приложение BIND_NOTIFICATION_LISTENER_SERVICE разрешение и предоставляется явное предоставление доступа уведомления от пользователя:

@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 в приведенном выше коде может быть просто пустой класс.

import android.service.notification.NotificationListenerService;

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

С соответствующим разделом в манифест:

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

поскольку цель события явна, это, вероятно, должно избежать любого побочного эффекта запуска медиаплеера.

Примечание: сервер связи может быть не сразу активен после события звонка. Для этого, чтобы работать надежно, это может быть полезно для приложения, чтобы реализовать MediaSessionManager.OnActiveSessionsChangedListener чтобы отслеживать, когда сервер связи становится активным, перед отправкой события.

обновление:

на Android O, нужно имитировать ACTION_DOWN до ACTION_UP, в противном случае вышеизложенное не имеет никакого эффекта. т. е. необходимо следующее:

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

но поскольку официальный вызов для ответа на вызов доступен с Android O (см. Верхний ответ), больше не может быть необходимости в этом взломе, если только вы не застряли со старым уровнем API компиляции перед Android O.

чтобы немного уточнить ответ @Muzikant и немного изменить его, чтобы работать немного чище на моем устройстве, попробуйте input keyevent 79, постоянной для KeyEvent.KEYCODE_HEADSETHOOK. очень грубо:

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

простите довольно плохие соглашения о кодировании, я не слишком хорошо разбираюсь во времени выполнения.exec() делает вызов. Обратите внимание, что мое устройство не укоренено, и я не запрашиваю привилегии root.

проблема с этим подходом заключается в том, что он работает только под определенные условия (для меня). То есть, если я запускаю вышеупомянутый поток из опции меню, которую пользователь выбирает во время вызова, вызов отвечает просто отлично. Если я запускаю его с приемника, который отслеживает состояние входящего вызова, он полностью игнорируется.

Так что на моем Nexus 5 он хорошо работает для пользовательского ответа и должен соответствовать цели пользовательского экрана вызова. Это просто не будет работать для любого вида автоматизированных приложений типа управления вызовами.

также следует отметить все возможные предостережения, включая то, что это тоже, вероятно, перестанет работать в обновлении или двух.

с помощью команды adb Как ответить на звонок от adb

имейте в виду, что Android-это Linux с массивной JVM на переднем конце. Вы можете скачать приложение командной строки и запустить телефон, и теперь у вас есть обычный компьютер Linux и командная строка, которая делает все нормальные вещи. Запуск скриптов, Вы можете даже ssh к нему (OpenVPN трюк)

спасибо @notz ответ работает для меня на Lolillop. Для того, чтобы этот код работал со старым Android SDK, вы можете сделать этот код:

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

Как включить динамик телефона после автоматического ответа на звонки.

Я решил свою проблему выше с setSpeakerphoneOn. Я думаю, что его стоит разместить здесь, так как вариант использования для автоматического ответа на телефонный звонок часто также требует громкой связи, чтобы быть полезным. Еще раз спасибо всем в этой теме, Какая потрясающая работа.

это работает для меня на Android 5.1.1 на Nexus 4 без корня. ;)

разрешения требуется:

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

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
}

выполните следующую команду от имени root:

input keyevent 5

подробнее о моделировании ключевых событий здесь.

можно использовать этот базовый класс, который я создал для запуска команд от имени root из вашего приложения.

FYI если вы заинтересованы в том, как закончить текущий вызов на Android O, Valter's Method 1: TelephonyManager.answerRingingCall() работает, если вы измените метод, который вы вызываете, чтобы быть endCall.

для этого требуется только android.permission.CALL_PHONE разрешения.

вот код:

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