Як можна програмно відповідати на вхідні дзвінки в 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 було структуровано. Це означає, що якщо ви не підтримуєте лише пристрої з льодяником, що, мабуть, є поганим рішенням, виходячи з крихітної частки його ринку на даний момент, вам все одно потрібно надати резервні методи, якщо йти цим шляхом.

Як би ми це використали?

Оскільки розглянутий метод прихований від використання програм 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>для нього, але це не принесе користі, оскільки рівень захисту для цього дозволу - це система підпису | ( див. Рядок 1201 ядра / AndroidManifest на 5.0.0_r1 ).

Ви можете прочитати випуск 34785: Оновлення документації для android: protectionLevel, яка була створена ще в 2012 році, щоб побачити, що нам бракує деталей про конкретний "синтаксис конвеєра", але, експериментуючи навколо, здається, він повинен функціонувати як "І", що означає всі зазначені прапори повинні бути виконані для отримання дозволу. Працюючи з цим припущенням, це означало б, що ви повинні мати свою заявку:

  1. Встановлюється як системний додаток.

    Це повинно бути добре, і це можна зробити, попросивши користувачів встановити за допомогою ZIP-коду під час відновлення, наприклад, при рутуванні або встановленні програм Google на власні ПЗУ, в яких вони ще не упаковані.

  2. Підписано тим самим підписом, що і фреймворки / база, відома як система, вона ж ПЗУ.

    Тут виникають проблеми. Для цього потрібно взяти в руки клавіші, що використовуються для підписання фреймворків / бази. Вам потрібно було б не тільки отримати доступ до ключів 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.

Як ми можемо використовувати це програмно?

Java

Наступний код - це груба реалізація, яка виконується як доказ концепції. Якщо ви дійсно хочете продовжувати використовувати цей метод, ви, мабуть, хочете ознайомитися з рекомендаціями щодо безпроблемного використання 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 . Метод навіть не вимагає кореневого доступу, що робить його придатним для поширених випадків широкого загалу, але в методі є невеликий недолік - подія натискання кнопки гарнітури не може бути вказана, щоб вимагати дозволу, це означає, що вона працює як справжня натискання кнопок і бульбашки вгору по всьому ланцюжку, що, в свою чергу, означає, що ви повинні бути обережними щодо того, коли імітувати натискання кнопки, оскільки це може, наприклад, викликати музичний плеєр для початку відтворення, якщо ніхто інший з вищим пріоритетом не готовий подія.

Код?

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; д-р

Існує приємний загальнодоступний 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);

1
де mi suppoz, щоб додати код у розділі "Нарешті поклич магію". Чи буде це працювати для Android 6.0
Akshay Shah

Я прийшов сюди, щоб сказати, що broadcastHeadsetConnected (логічне підключення) - це те, що вирішило проблему на пристрої Samsung A3 2016. Без цього дуже подібний метод (з використанням окремих, прозорих дій та потоків для викликів та іншого) працював повністю для близько 20 перевірених пристроїв, тоді цей A3 з’явився і змусив мене перевірити це питання на нові відповіді. Після порівняння з моїм кодом, це була суттєва різниця!
leRobot

1
Як я також можу відхилити дзвінок? Чи можете ви оновити відповідь, щоб показати це?
Амані

@leRobot Ця відповідь перевіряє, чи це пристрій HTC для трансляціїHeadsetConnected, як ви можете перевірити, чи це пристрій Samsung A3 2016? До речі, це дійсно хороша відповідь, мій додаток може відповісти на телефонний дзвінок, навіть екран заблокований.
eepty

@eepty Ви можете використовувати офіційне посилання на пристрій для збору даних. ( support.google.com/googleplay/answer/1727131?hl=uk ). Я використовую 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
Це вимагає додаткового кроку явного надання дозволу користувачем десь у меню налаштувань залежно від системи, крім прийняття дозволу в маніфесті.
хедпак

повідомляючи, що це працює у вузькому випадку: Galaxy A3 2016 з Marshmallow. Я тестуватиму це в групі пристроїв A3, які не працюють із методом введення ключів через FATAL EXCEPTION: 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-прав.

Проблема такого підходу полягає в тому, що він працює лише за певних умов (для мене). Тобто, якщо я запускаю вищезазначений потік з опції меню, яку користувач вибирає під час дзвінка, дзвінок відповідає просто чудово. Якщо я запускаю його з приймача, який контролює стан вхідних дзвінків, він повністю ігнорується.

Отже, на моєму Nexus 5 він добре працює для відповіді, керованого користувачем, і повинен відповідати призначеному для користувача екрану дзвінків. Це просто не буде працювати для будь-яких автоматизованих додатків типу керування дзвінками.

Також слід зазначити всі можливі застереження, включаючи те, що це теж, ймовірно, перестане працювати через оновлення або два.


input keyevent 79чудово працює на Sony Xperia 5.0. Працює під час дзвінка з активності або з приймача мовлення.
Ніколас

0

за допомогою команд adb Як прийняти дзвінок за допомогою adb

Майте на увазі, що Android - це Linux з масивною JVM на передній панелі. Ви можете завантажити програму командного рядка та скористатися телефоном, і тепер у вас є звичайний комп'ютер Linux та командний рядок, який виконує всі звичайні дії. Запускайте скрипти, до них можна навіть ssh (фокус OpenVPN)


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

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, що, я сумніваюся, ніби це погано поводиться для звичайного користувача, виклик, мабуть, просто не вдасться і не викликати іншу дію.
Вальтер Янсонс,

-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

FYI, якщо вас цікавить, як ЗАКІНЧИТИ поточний дзвінок на Android O, Вальтер 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);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.