W jaki sposób można programowo odbierać połączenia przychodzące w systemie Android 5.0 (Lollipop)?


87

Kiedy próbuję utworzyć niestandardowy ekran dla połączeń przychodzących, próbuję programowo odebrać połączenie przychodzące. Używam następującego kodu, ale nie działa on w systemie Android 5.0.

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

// froyo and beyond trigger on buttonUp instead of buttonDown
Intent buttonUp = new Intent(Intent.ACTION_MEDIA_BUTTON);               
buttonUp.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
context.sendOrderedBroadcast(buttonUp, "android.permission.CALL_PRIVILEGED");

Oh Człowieku, po co w to wpadać, po prostu przesuń Człowieka! wydaje mi się łatwiejsze \ m /
nobalG

Tworzę niestandardowy ekran połączeń przychodzących dla użytkowników Androida.
Maveroid

2
Ktoś? Mnie też to interesuje! Próbowałem wielu rzeczy, ale nie działały: /
Arthur

1
@nobalG mówi programowo
dsharew

1
@maveroid, czy znalazłeś obejście dla Androida 5.0?
arthursfreire

Odpowiedzi:


155

Zaktualizuj do Androida 8.0 Oreo

Mimo że pierwotnie pytanie dotyczyło obsługi Androida L, ludzie nadal wydają się trafiać na to pytanie i odpowiedź, dlatego warto opisać ulepszenia wprowadzone w systemie Android 8.0 Oreo. Metody kompatybilności wstecznej są nadal opisane poniżej.

Co się zmieniło?

Począwszy od Androida 8.0 Oreo , grupa uprawnień PHONE zawiera również uprawnienie ANSWER_PHONE_CALLS . Jak sugeruje nazwa uprawnienia, przytrzymanie go umożliwia aplikacji programowe akceptowanie połączeń przychodzących za pośrednictwem odpowiedniego wywołania interfejsu API bez hakowania systemu przy użyciu odbicia lub symulowania użytkownika.

Jak wykorzystujemy tę zmianę?

Jeśli obsługujesz starsze wersje systemu Android, sprawdź wersję systemu w czasie wykonywania , aby móc hermetyzować to nowe wywołanie interfejsu API, zachowując jednocześnie obsługę starszych wersji systemu Android. Powinieneś postępować zgodnie z żądaniem uprawnień w czasie wykonywania, aby uzyskać nowe uprawnienia w czasie wykonywania, co jest standardem w nowszych wersjach Androida.

Po uzyskaniu pozwolenia aplikacja musi po prostu wywołać metodę acceptRingingCall operatora TelecomManager . Podstawowe wywołanie wygląda więc następująco:

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

Metoda 1: TelephonyManager.answerRingingCall ()

Gdy masz nieograniczoną kontrolę nad urządzeniem.

Co to jest?

Istnieje TelephonyManager.answerRingingCall (), która jest ukrytą, wewnętrzną metodą. Działa jako pomost dla ITelephony.answerRingingCall (), który został omówiony w interwebach i wydaje się obiecujący na początku. To jest nie dostępny na 4.4.2_r1 jak została ona wprowadzona dopiero w popełnić 83da75d na Androidzie 4.4 KitKat ( linia 1537 na 4.4.3_r1 ), a później „ponownie” w popełnić f1e1e77 dla Lollipop ( linia 3138 na 5.0.0_r1 ) ze względu na sposób Drzewo Git zostało ustrukturyzowane. Oznacza to, że jeśli nie obsługujesz tylko urządzeń z Lollipopem, co jest prawdopodobnie złą decyzją ze względu na niewielki udział w rynku w tej chwili, nadal musisz zapewnić metody awaryjne, jeśli pójdziesz tą drogą.

Jak byśmy to wykorzystali?

Ponieważ omawiana metoda jest niewidoczna dla aplikacji SDK, należy użyć odbicia, aby dynamicznie zbadać i używać metody w czasie wykonywania. Jeśli nie jesteś zaznajomiony z refleksją, możesz szybko przeczytać Co to jest refleksja i dlaczego jest przydatna? . Możesz również zagłębić się w szczegóły w Trail: The Reflection API, jeśli chcesz to zrobić.

A jak to wygląda w kodzie?

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

To jest zbyt piękne by było prawdziwe!

Właściwie jest jeden drobny problem. Ta metoda powinna być w pełni funkcjonalna, ale menedżer bezpieczeństwa chce, aby dzwoniący zatrzymywali android.permission.MODIFY_PHONE_STATE . To uprawnienie dotyczy tylko częściowo udokumentowanych funkcji systemu, ponieważ osoby trzecie nie powinny go dotykać (jak widać w dokumentacji). Możesz spróbować dodać <uses-permission>do niego a, ale to nic nie da, ponieważ poziom ochrony dla tego uprawnienia to signature | system ( patrz wiersz 1201 core / AndroidManifest w wersji 5.0.0_r1 ).

Możesz przeczytać numer 34785: Aktualizacja dokumentacji android: protectionLevel, która została utworzona w 2012 roku, aby zobaczyć, że brakuje nam szczegółów na temat określonej „składni potoku”, ale po eksperymentach wydaje się, że musi ona działać jako „AND”, co oznacza wszystkie określone flagi muszą być spełnione, aby zezwolenie zostało udzielone. Przy takim założeniu oznaczałoby to, że musisz mieć swoją aplikację:

  1. Zainstalowana jako aplikacja systemowa.

    Powinno to być w porządku i można to osiągnąć, prosząc użytkowników o zainstalowanie przy użyciu ZIP podczas odzyskiwania, na przykład podczas rootowania lub instalowania aplikacji Google na niestandardowych ROM-ach, które nie mają ich jeszcze w pakiecie.

  2. Podpisany tym samym podpisem, co frameworki / podstawa, czyli system, czyli ROM.

    Tu pojawiają się problemy. Aby to zrobić, musisz mieć ręce na klawiszach używanych do podpisywania struktur / bazy. Musiałbyś nie tylko uzyskać dostęp do kluczy Google do obrazów fabrycznych Nexusa, ale musiałbyś również uzyskać dostęp do kluczy wszystkich innych producentów OEM i programistów ROM. Nie wydaje się to prawdopodobne, więc możesz podpisać aplikację za pomocą kluczy systemowych, tworząc niestandardową pamięć ROM i prosząc użytkowników o przełączenie się na nią (co może być trudne) lub znajdując exploit, za pomocą którego można ominąć poziom ochrony uprawnień (co również może być trudne).

Ponadto wydaje się, że to zachowanie jest związane z problemem 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS już nie działa, który wykorzystuje ten sam poziom ochrony wraz z nieudokumentowaną flagą programistyczną.

Praca z TelephonyManager brzmi dobrze, ale nie będzie działać, jeśli nie uzyskasz odpowiedniego pozwolenia, co w praktyce nie jest takie łatwe.

A co z korzystaniem z TelephonyManager w inny sposób?

Niestety, wydaje się, że musisz przytrzymać android.permission.MODIFY_PHONE_STATE, aby użyć fajnych narzędzi, co z kolei oznacza, że ​​będziesz mieć trudności z uzyskaniem dostępu do tych metod.


Metoda 2: wezwanie serwisu KOD SERWISOWY

Kiedy możesz przetestować, że kompilacja uruchomiona na urządzeniu będzie działać z określonym kodem.

Bez możliwości interakcji z TelephonyManager istnieje również możliwość interakcji z usługą za pośrednictwem servicepliku wykonywalnego.

Jak to działa?

Jest to dość proste, ale dokumentacji na temat tej trasy jest jeszcze mniej niż innych. Wiemy na pewno, że plik wykonywalny przyjmuje dwa argumenty - nazwę usługi i kod.

  • Nazwa usługi, której chcemy użyć, to telefon .

    Można to zobaczyć, biegnąc service list.

  • Kod chcemy użytku wydaje się być 6 , ale wydaje się być teraz 5 .

    Wygląda na to, że został oparty na IBinder.FIRST_CALL_TRANSACTION + 5 dla wielu wersji teraz (od 1.5_r4 do 4.4.4_r1 ), ale podczas lokalnych testów kod 5 działał, aby odebrać połączenie przychodzące. Ponieważ Lollipo to ogromna aktualizacja dookoła, zrozumiałe jest, że tutaj również uległy zmianie elementy wewnętrzne.

Wynika to z polecenia service call phone 5.

Jak używamy tego programowo?

Jawa

Poniższy kod to przybliżona implementacja, która ma służyć jako dowód słuszności koncepcji. Jeśli rzeczywiście chcesz skorzystać z tej metody, prawdopodobnie zechcesz zapoznać się ze wskazówkami dotyczącymi bezproblemowego korzystania z su i prawdopodobnie przejść na pełniejszą wersję libsuperuser firmy 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();
}

Oczywisty

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

Czy to naprawdę wymaga dostępu do roota?

Niestety, na to wygląda. Możesz spróbować użyć na nim Runtime.exec , ale nie udało mi się znaleźć szczęścia na tej trasie.

Jak stabilne jest to?

Cieszę się, że zapytałeś. Ze względu na brak udokumentowania może to występować w różnych wersjach, co ilustruje pozorna różnica w kodzie powyżej. Nazwa usługi prawdopodobnie powinna pozostać telefonem w różnych kompilacjach, ale z tego, co wiemy, wartość kodu może się zmieniać w wielu kompilacjach tej samej wersji (wewnętrzne modyfikacje przez, powiedzmy, skórkę producenta OEM), co z kolei łamie zastosowaną metodę. Warto zatem wspomnieć, że testy odbyły się na Nexusie 4 (mako / occam). Osobiście odradzałbym stosowanie tej metody, ale ponieważ nie jestem w stanie znaleźć bardziej stabilnej metody, uważam, że jest to najlepszy strzał.


Oryginalna metoda: intencje kodu klucza zestawu słuchawkowego

Na chwile, kiedy musisz się ustatkować.

Poniższy rozdział był pod silnym wpływem tej odpowiedzi przez Riley C .

Wydaje się, że metoda symulowanego zamiaru zestawu słuchawkowego, przedstawiona w pierwotnym pytaniu, jest nadawana tak, jak można by się spodziewać, ale nie wydaje się, aby spełniała ona cel, jakim jest odebranie połączenia. Chociaż wydaje się, że istnieje kod, który powinien obsługiwać te zamiary, po prostu nie dbamy o to, co musi oznaczać, że muszą istnieć jakieś nowe środki zaradcze przeciwko tej metodzie. Dziennik również nie pokazuje nic interesującego i osobiście nie wierzę, że przekopywanie się przez źródło Androida w tym celu będzie opłacalne tylko ze względu na możliwość wprowadzenia przez Google niewielkiej zmiany, która i tak łatwo złamie zastosowaną metodę.

Czy jest coś, co możemy teraz zrobić?

Zachowanie można konsekwentnie odtwarzać za pomocą wejściowego pliku wykonywalnego. Pobiera argument kodu klucza, dla którego po prostu przekazujemy KeyEvent.KEYCODE_HEADSETHOOK . Ta metoda nie wymaga nawet dostępu do konta roota, dzięki czemu nadaje się do powszechnych zastosowań w społeczeństwie, ale ma małą wadę - nie można określić zdarzenia naciśnięcia przycisku zestawu słuchawkowego jako wymagającego pozwolenia, co oznacza, że ​​działa jak prawdziwy naciśnięcie przycisku i bulgotanie w całym łańcuchu, co z kolei oznacza, że ​​musisz uważać, kiedy symulować naciśnięcie przycisku, ponieważ może to na przykład spowodować uruchomienie odtwarzacza muzyki, jeśli nikt inny o wyższym priorytecie nie jest gotowy do obsługi wydarzenie.

Kod?

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

Istnieje ładny publiczny interfejs API dla systemu Android 8.0 Oreo i nowszych.

Nie ma publicznego interfejsu API przed wersją Android 8.0 Oreo. Wewnętrzne interfejsy API są zabronione lub po prostu bez dokumentacji. Należy postępować ostrożnie.


Jeśli chodzi o intencje kodu klucza zestawu słuchawkowego, czy sprawdziłeś tutaj źródło Google z jakiegokolwiek powodu, dla którego przestałyby być wykorzystywane? Zabawne jest to, że połączenia można nadal łatwo odrzucać za pomocą tych zamiarów (po prostu naśladuj długie naciśnięcie), ale nic nie działa, aby odebrać. Nie znalazłem jeszcze wyraźnego sprawdzenia pozwolenia lub innej potencjalnej blokady i mam nadzieję, że druga para oczu może coś odkryć.
Riley C

Byłem trochę zajęty, stąd opóźnienie - spróbuję poświęcić trochę czasu na rozwiązanie tego problemu. Po szybkim spojrzeniu wygląda na to, że CallsManager konstruuje HeadsetMediaButton. Wywołanie zwrotne sesji powinno zadbać o wywołanie handleHeadsetHook (KeyEvent) po wywołaniu zwrotnym z MediaSessionManager. Wygląda na to, że cały kod pasuje ... ale zastanawiam się, czy ktoś mógłby usunąć KeyEvent.ACTION_DOWN, który ma zamiar przetestować? (Oznacza to, że uruchom KeyEvent.ACTION_UP tylko jeden raz.)
Valter Jansons,

W rzeczywistości HeadsetMediaButton współpracuje z MediaSession i nie współpracuje bezpośrednio z MediaSessionManager ...
Valter Jansons,

1
Dla tych z Was, którym udało się znaleźć sytuację, w której oryginalna metoda wydaje się nie działać (np. Lollipop ma problemy), mam dobrą i złą wiadomość: udało mi się sprawić, aby ACTION_UP działał w 100% w moim kodzie, z FULL_WAKE_LOCK. Nie będzie działać z PARTIAL_WAKE_LOCK. Nie ma absolutnie żadnej dokumentacji na temat przyczyn takiego stanu rzeczy. Wyszczególnię to w przyszłej odpowiedzi, kiedy dokładniej przetestuję kod mojego eksperymentu. Zła wiadomość jest taka, że ​​FULL_WAKE_LOCK jest przestarzały, więc jest to poprawka, która będzie działać tylko tak długo, jak Google będzie ją przechowywać w API.
leRobot

1
W wielu przypadkach nie jest wywoływany haczyk z oryginalnej odpowiedzi. Zauważyłem, że lepiej jest po prostu najpierw wywołać exec, a potem i tak zaraz potem wywołać przycisk w górę.
Warpzit,

36

W pełni działające rozwiązanie oparte jest na kodzie @Valter Strods.

Aby to działało, musisz wyświetlić (niewidoczne) działanie na ekranie blokady, na którym wykonywany jest kod.

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>

Aktywność akceptacji połączenia

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

Styl

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

Wreszcie wezwij magię!

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
gdzie muszę dodać kod w sekcji „Wreszcie wezwij magię”. Czy będzie działać na Androidzie 6.0
Akshay Shah

Przyszedłem tutaj, aby powiedzieć, że broadcastHeadsetConnected (połączenie boolean) rozwiązało problem w urządzeniu Samsung A3 2016. Bez tego bardzo podobna metoda (wykorzystująca osobną, przezroczystą aktywność i wątki do wywołań itp.) Działała w pełni dla około 20 testowanych urządzeń, wtedy pojawił się ten A3 i zmusił mnie do ponownego sprawdzenia tego pytania w poszukiwaniu nowych odpowiedzi. Po porównaniu z moim kodem była to znacząca różnica!
leRobot

1
Jak mogę również odrzucić połączenie? Czy możesz zaktualizować odpowiedź, aby to pokazać?
Amanni

@leRobot W tej odpowiedzi sprawdź, czy jest to urządzenie HTC do nadawania HeadsetConnected, jak możesz sprawdzić, czy jest to urządzenie Samsung A3 2016? Nawiasem mówiąc, to naprawdę dobra odpowiedź, moja aplikacja może odbierać połączenia telefoniczne, nawet jeśli ekran jest zablokowany.
eepty

@eepty W przypadku danych kompilacji możesz użyć oficjalnego odniesienia do urządzenia. ( support.google.com/googleplay/answer/1727131?hl=en ). Używam Build.MODEL.startsWith („SM-A310”) dla A3 2016. ALE! Mogę potwierdzić, że A3 2016 nie obsługuje transmisji podłączonych do zestawu słuchawkowego! To, co faktycznie rozwiązało mój problem, to zmiana kolejności, tak aby Runtime.getRuntime (). Exec (... wyzwalał jako pierwszy dla tych urządzeń. Wydaje się, że działa za każdym razem dla tego urządzenia i nie cofa się do wyjątku.
leRobot

14

Poniżej przedstawiono alternatywne podejście, które zadziałało dla mnie. Wysyła kluczowe zdarzenie do serwera telekomunikacyjnego bezpośrednio za pomocą interfejsu API MediaController. Wymaga to, aby aplikacja miała uprawnienia BIND_NOTIFICATION_LISTENER_SERVICE i otrzymała wyraźne zezwolenie na dostęp do powiadomień od użytkownika:

@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 w powyższym kodzie może być po prostu pusta klasa.

import android.service.notification.NotificationListenerService;

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

Z odpowiednią sekcją w manifeście:

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

Ponieważ cel zdarzenia jest wyraźny, powinno to prawdopodobnie uniknąć jakiegokolwiek efektu ubocznego uruchamiania odtwarzacza multimedialnego.

Uwaga: serwer telekomunikacyjny może nie być aktywny natychmiast po zdarzeniu dzwonienia. Aby to działało niezawodnie, może być przydatne, aby aplikacja zaimplementowała MediaSessionManager.OnActiveSessionsChangedListener w celu monitorowania, kiedy serwer telekomunikacyjny staje się aktywny, przed wysłaniem zdarzenia.

Aktualizacja:

W Androidzie O trzeba symulować ACTION_DOWNwcześniej ACTION_UP, w przeciwnym razie powyższe nie daje efektu. tzn. potrzebne są:

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

Ale ponieważ oficjalne wezwanie do odebrania połączenia jest dostępne od Androida O (patrz najlepsza odpowiedź), może nie być już potrzeby tego hackowania, chyba że utkniesz ze starym poziomem kompilacji API przed Androidem O.


Na mnie to nie wyszło. Zwrócił błąd uprawnień. Aplikacja nie ma dostępu do powiadomień. Używam Androida L
Jame

2
Wymaga to dodatkowego kroku, polegającego na wyraźnym nadaniu uprawnień przez użytkownika, gdzieś w menu ustawień w zależności od systemu, poza zaakceptowaniem zgody w manifeście.
headuck

zgłoszenie tego wydaje się działać w wąskim przypadku: Galaxy A3 2016 z Marshmallow. Będę to testować na grupie urządzeń A3, które nie działają z metodą wejścia klucza zdarzenia z powodu KRYTYCZNEGO WYJĄTKU: java.lang.SecurityException: Wstrzyknięcie do innej aplikacji wymaga uprawnienia INJECT_EVENTS. Urządzenia, których dotyczy problem, stanowią około 2% mojej bazy użytkowników i nie replikuję ich wyjątku, ale spróbuję skorzystać z tej metody, aby sprawdzić, czy uda im się odebrać połączenie. Na szczęście moja aplikacja żąda już wyraźnego powiadomienia. dostęp do innych celów.
leRobot

po szeroko zakrojonych testach z przyjemnością informuję, że urządzenia A3 2016, które nie działały z exec "input keyevent", działały z metodą MediaController # dispatchMediaButtonEvent (<hook KeryEvent>)). to oczywiście działa tylko wtedy, gdy użytkownik zezwoli na jawny dostęp do powiadomień, więc musisz dodać ekran kierujący do Ustawień Androida i zasadniczo potrzebujesz, aby użytkownik wykonał w tym celu dodatkowe działania, jak opisano szczegółowo w odpowiedzi. W mojej aplikacji podjęliśmy dodatkowe kroki, aby nadal pytać, czy użytkownik przechodzi do tego ekranu, ale nie dodaje powiadomienia. dostęp
leRobot

Działa to na Androidzie Nougat. Rozwiązanie @notz działa świetnie w przeciwnym razie, ale narzeka „Tylko system może wysłać zdarzenie klucza multimedialnego do globalnej sesji priorytetowej” w systemie Android 7.
Peng Bai

9

Aby nieco rozwinąć odpowiedź @Muzikant i nieco ją zmodyfikować, aby działała nieco czysto na moim urządzeniu, wypróbuj input keyevent 79stałą dla KeyEvent.KEYCODE_HEADSETHOOK . Z grubsza:

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

Wybaczcie dość złe konwencje kodowania, nie jestem zbyt dobrze zorientowany w wywołaniach Runtime.exec (). Zwróć uwagę, że moje urządzenie nie jest zrootowane ani nie żądam uprawnień roota.

Problem z tym podejściem polega na tym, że działa tylko w określonych warunkach (jak dla mnie). Oznacza to, że jeśli uruchomię powyższy wątek z opcji menu wybranej przez użytkownika podczas dzwonienia połączenia, połączenie zostanie odebrane poprawnie. Jeśli uruchomię go z odbiornika, który monitoruje stan połączenia przychodzącego, zostanie całkowicie zignorowany.

Tak więc na moim Nexusie 5 działa dobrze w przypadku odbierania przez użytkownika i powinien pasować do niestandardowego ekranu połączenia. Po prostu nie będzie działać w przypadku żadnych zautomatyzowanych aplikacji sterujących połączeniami.

Warto również zwrócić uwagę na wszystkie możliwe zastrzeżenia, w tym również to, że prawdopodobnie przestanie działać w jednej lub dwóch aktualizacji.


input keyevent 79działa dobrze na Sony Xperia 5.0. Działa podczas dzwonienia z aktywności lub z odbiornika.
nicolas

0

za pomocą poleceń adb Jak odebrać połączenie przez adb

Pamiętaj, że Android to Linux z ogromną maszyną JVM z przodu. Możesz pobrać aplikację wiersza poleceń i zrootować telefon, a teraz masz zwykły komputer z systemem Linux i wiersz poleceń, który wykonuje wszystkie normalne czynności. Uruchamiaj skrypty, możesz nawet ssh do niego (sztuczka OpenVPN)


0

Dzięki @notz, odpowiedź działa dla mnie na Lolillop. Aby kod działał ze starym zestawem SDK systemu Android, możesz wykonać ten kod:

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

Jak włączyć głośnik po automatycznym odebraniu połączeń.

Powyższy problem rozwiązałem za pomocą setSpeakerphoneOn. Myślę, że warto go tutaj opublikować, ponieważ przypadek użycia automatycznego odbierania połączenia telefonicznego często wymagałby również użycia zestawu głośnomówiącego. Jeszcze raz dziękuję wszystkim w tym wątku za wspaniałą pracę.

To działa dla mnie na Androidzie 5.1.1 na moim Nexusie 4 bez ROOT. ;)

Wymagane pozwolenie:

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

Kod 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
Ciekawy. Właściwie to próbuję razem odebrać połączenie i włączyć głośnik, więc takie podejście wydaje się rozwiązywać oba problemy :). Mam jednak podobne pytanie, jak niektóre komentarze w innych odpowiedziach: gdzie idzie ten kod?
fangmobile

-1

Uruchom następujące polecenie jako root:

input keyevent 5

Więcej szczegółów na temat symulacji kluczowych wydarzeń tutaj .

Możesz użyć tej klasy bazowej, którą utworzyłem, do uruchamiania poleceń jako root z Twojej aplikacji.


1
Podczas testowania ze zwykłym profilem użytkownika wywołało to dla mnie interfejs połączenia, prosząc mnie o przesunięcie palcem w lewo / w prawo, aby odrzucić / odebrać lub użyć szybkiej akcji / odpowiedzi. Jeśli OP tworzy niestandardowy ekran połączenia przychodzącego , nie jest to naprawdę pomocne, chyba że zachowuje się inaczej pod rootem, w co wątpię, jakby nie zachowywał się dobrze dla zwykłego użytkownika, połączenie prawdopodobnie po prostu się nie powiedzie, a nie wywołać inną akcję.
Valter Jansons,

-2

przetestuj to: najpierw dodaj uprawnienia, a następnie użyj funkcji killCall (), aby się rozłączyć, użyj funkcji answerCall (), aby odebrać połączenie

<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

Do Twojej wiadomości, jeśli interesuje Cię, jak zakończyć trwające połączenie na Androidzie O, Valter zadziała, Method 1: TelephonyManager.answerRingingCall()jeśli zmienisz wywoływaną metodę endCall.

Wymaga tylko android.permission.CALL_PHONEpozwolenia.

Oto kod:

// 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);
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.