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

87

Поскольку я пытаюсь создать собственный экран для входящих вызовов, я пытаюсь программно отвечать на входящий вызов. Я использую следующий код, но он не работает в 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");
мавероид
источник
о, чувак, зачем наткнуться на это, просто сдвинуть человека! мне кажется проще \ m /
nobalG
Я создаю собственный экран входящего вызова для пользователей Android.
maveroid
2
Кто угодно? Мне тоже это интересно! Много чего пробовал, но ничего не вышло: /
Артур
1
@nobalG он говорит программно
dsharew
1
@maveroid, вы придумали обходной путь для Android 5.0?
arthursfreire

Ответы:

155

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

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

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

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

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

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

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

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 (), который обсуждался в Интернете и с самого начала кажется многообещающим. Это не доступно на 4.4.2_r1 , как она была введена только в фиксации 83da75d для Android 4.4 KitKat ( строка 1537 на 4.4.3_r1 ) , а затем «повторно» в фиксации 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.permission.MODIFY_PHONE_STATE . Это разрешение относится только к частично задокументированным функциям системы, поскольку третьи стороны не должны трогать его (как вы можете видеть из документации к нему). Вы можете попробовать добавить <uses-permission>для него, но это не поможет, поскольку уровень защиты для этого разрешения - signature | system ( см. Строку 1201 core / AndroidManifest на 5.0.0_r1 ).

Вы можете прочитать Документацию о проблеме 34785: Обновление android: protectionLevel, которая была создана еще в 2012 году, чтобы увидеть, что нам не хватает деталей о конкретном «синтаксисе канала», но, поэкспериментировав, выяснилось, что он должен работать как «И», что означает все указанные флаги должны быть выполнены для предоставления разрешения. При таком предположении это будет означать, что у вас должно быть приложение:

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

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

  2. Подписан той же подписью, что и frameworks / base, также известный как система, он же ROM.

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

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

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

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

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


Метод 2: вызов службы СЕРВИСНЫЙ КОД

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

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

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

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

  • Имя службы, которое мы хотим использовать, - телефон .

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

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

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

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

Как это использовать программно?

Ява

Следующий код является приблизительной реализацией, призванной служить доказательством концепции. Если вы действительно хотите продолжить и использовать этот метод, вы, вероятно, захотите ознакомиться с инструкциями по беспроблемному использованию su и, возможно, переключиться на более полностью разработанный libsuperuser от 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();
}

Манифест

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

Для этого действительно нужен root-доступ?

К сожалению, это так. Вы можете попробовать использовать на нем Runtime.exec , но мне не повезло с этим маршрутом.

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

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


Исходный метод: назначение кода гарнитуры

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

В следующем разделе под сильным влиянием этого ответа по Райли C .

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

Что мы можем сделать прямо сейчас?

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

Код?

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 и новее.

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

Вальтер Янсонс
источник
Что касается намерений кода клавиатуры гарнитуры, проверяли ли вы здесь источник Google по какой-либо причине, по которой они перестают действовать? Забавно то, что вызовы все еще можно легко отклонить с помощью этих намерений (просто имитируйте долгое нажатие), но ничего не получается ответить. Мне еще предстоит найти явную проверку разрешений или другой потенциальный блок, и я надеюсь, что вторая пара глаз может что-то обнаружить.
Riley C
Был немного занят, отсюда задержка - постараюсь потратить некоторое время на то, чтобы это выяснить. После беглого взгляда кажется, что CallsManager создает HeadsetMediaButton. Обратный вызов сеанса должен заботиться о вызове handleHeadsetHook (KeyEvent) при обратных вызовах от MediaSessionManager. Кажется, что весь код совпадает ... но мне интересно, может ли кто-нибудь удалить намерение KeyEvent.ACTION_DOWN для тестирования? (То есть запускайте KeyEvent.ACTION_UP только один раз.)
Вальтер Янсонс,
На самом деле HeadsetMediaButton работает с MediaSession и не взаимодействует напрямую с MediaSessionManager ...
Вальтер Янсонс
1
Для тех из вас, кому удалось найти ситуацию, в которой оригинальный метод не работает (например, у Lollipop есть проблемы), у меня есть хорошие и плохие новости: мне удалось заставить ACTION_UP работать на 100% в моем коде с помощью FULL_WAKE_LOCK. Это не будет работать с PARTIAL_WAKE_LOCK. Нет абсолютно никакой документации о причинах этого. Я подробно расскажу об этом в будущем ответе, когда я более тщательно протестирую свой экспериментальный код. Плохая новость заключается в том, что FULL_WAKE_LOCK устарел, поэтому это исправление, которое будет действовать только до тех пор, пока Google хранит его в API.
leRobot
1
Уловка из исходного ответа во многих случаях не вызывается. Я обнаружил, что лучше сначала просто вызвать exec, а затем сразу после этого вызвать кнопку вверх.
Warpzit
36

Полностью рабочее решение основано на коде @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>

Активность приема звонков

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);
notz
источник
1
где mi suppoz добавить код в разделе «Наконец-то вызвать волшебство». Будет ли работать на Android 6.0
Акшай Шах
Я пришел сюда, чтобы сказать, что broadcastHeadsetConnected (логическое соединение) решена проблема в устройстве Samsung A3 2016. Без этого очень похожий метод (с использованием отдельных прозрачных действий и потоков для вызовов и прочего) полностью работал примерно на 20 протестированных устройствах, затем появился этот A3 и заставил меня перепроверить этот вопрос для получения новых ответов. После сравнения с моим кодом это была значительная разница!
leRobot
1
Как я могу отклонить звонок? Можете ли вы обновить ответ, чтобы показать это?
Amanni
@leRobot Этот ответ проверяет, является ли это устройством HTC для трансляцииHeadsetConnected, как вы можете проверить, является ли это устройством Samsung A3 2016? Кстати, это действительно хороший ответ, мое приложение может отвечать на телефонный звонок, даже если экран заблокирован.
eepty 07
@eepty Вы можете использовать официальный справочник устройства для данных сборки. ( support.google.com/googleplay/answer/1727131?hl=en ). Я использую Build.MODEL.startsWith ("SM-A310") для A3 2016. НО! Я могу подтвердить, что A3 2016 не поддерживает трансляцию через гарнитуру! Что на самом деле решило мою проблему, так это изменение порядка, чтобы Runtime.getRuntime (). Exec (... запускался первым для этих устройств. Кажется, что он работает каждый раз для этого устройства и не возвращается к исключению.
leRobot
14

Ниже приводится альтернативный подход, который сработал для меня. Он отправляет ключевое событие на телекоммуникационный сервер напрямую с помощью 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.

голова
источник
У меня не получилось. Он вернул ошибку разрешения. Доступ к уведомлению не предоставлен приложению. Я использую Android L
Джейм
2
Для этого требуется дополнительный шаг явного предоставления разрешения пользователем где-нибудь в меню настроек в зависимости от системы, помимо принятия разрешения в манифесте.
headuck
сообщение об этом, похоже, работает в узком случае: Galaxy A3 2016 с Marshmallow. Я буду тестировать это на группе устройств A3, которые не работают с методом input keyevent из-за ФАТАЛЬНОГО ИСКЛЮЧЕНИЯ: java.lang.SecurityException: для инъекции в другое приложение требуется разрешение INJECT_EVENTS. Нарушающие устройства составляют около 2% моей пользовательской базы, и я не реплицирую их исключение, но буду пытаться использовать этот метод, чтобы увидеть, удастся ли им принять вызов. К счастью, мое приложение уже запрашивает явное уведомление. доступ для других целей.
leRobot
после обширного тестирования я рад сообщить, что устройства A3 2016, которые не справлялись с exec "input keyevent", смогли работать с методом MediaController # dispatchMediaButtonEvent (<hook KeryEvent>)). это, очевидно, работает только после того, как пользователь разрешит явный доступ к уведомлениям, поэтому вам нужно будет добавить для этого экран, указывающий на настройки Android, и вам в основном нужно, чтобы пользователь предпринял дополнительные действия для этого, как подробно описано в ответе. В моем приложении мы предприняли дополнительные шаги, чтобы постоянно спрашивать об этом, если пользователь переходит на этот экран, но не добавляет уведомление. доступ
leRobot
Это работает на Android Nougat. Решение @notz отлично работает в остальном, но жалуется: «Только система может отправить событие медиа-ключа в сеанс глобального приоритета» на Android 7.
Пэн Бай
9

Чтобы немного уточнить ответ @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();

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

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

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

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

Райли С
источник
input keyevent 79отлично работает на Sony Xperia 5.0. Работает при звонке с занятия или с приемника вещания.
Николас
0

через команды adb Как ответить на звонок по adb

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

user1544207
источник
0

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

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
источник
0

Как включить громкую связь после автоматического ответа на звонки.

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

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

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

<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
}
Мадхава Джей
источник
1
Интересно. На самом деле я пытаюсь ответить на звонок и включить динамик вместе, поэтому этот подход, кажется, решает оба вопроса :). У меня есть вопрос, аналогичный некоторым комментариям в других ответах: куда идет этот код?
fangmobile
-1

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

input keyevent 5

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

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

Музикант
источник
1
Во время тестирования с обычным профилем пользователя это вызвало для меня пользовательский интерфейс во время разговора, в котором меня просили провести пальцем влево / вправо, чтобы отклонить / ответить или использовать быстрое действие / ответ. Если OP создает настраиваемый экран входящего вызова , это на самом деле не поможет, если только он не ведет себя по-другому под root, что я сомневаюсь, как если бы он не вел себя хорошо для обычного пользователя, вызов, вероятно, просто завершился бы ошибкой, а не вызвать другое действие.
Valter Jansons
-2

проверьте это: сначала добавьте разрешения, затем используйте killCall (), чтобы повесить трубку, используйте answerCall (), чтобы ответить на звонок

<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

К вашему сведению, если вас интересует, как завершить текущий вызов на Android O, Valter 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);
    }
}
Франсуа Дерму
источник