Bagaimana panggilan masuk dijawab secara terprogram di Android 5.0 (Lollipop)?

89

Saat saya mencoba membuat layar khusus untuk panggilan masuk, saya mencoba menjawab panggilan masuk secara terprogram. Saya menggunakan kode berikut tetapi tidak berfungsi di 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");
maveroid
sumber
Ya ampun, mengapa mengalami ini, geser saja Man! tampaknya lebih mudah bagi saya \ m /
nobalG
Saya membuat layar panggilan masuk khusus untuk pengguna android.
maveroid
2
Siapa saja? Saya juga tertarik dengan yang ini! Mencoba banyak hal, tetapi tidak berhasil: /
Arthur
1
@nobalG dia mengatakan secara terprogram
dsharew
1
@maveroid, Apakah Anda mendapatkan solusi untuk Android 5.0?
arthursfreire

Jawaban:

156

Perbarui dengan Android 8.0 Oreo

Meskipun pertanyaan tersebut awalnya diminta untuk dukungan Android L, orang-orang tampaknya masih mengajukan pertanyaan dan jawaban ini, jadi perlu dijelaskan peningkatan yang diperkenalkan di Android 8.0 Oreo. Metode kompatibel mundur masih dijelaskan di bawah ini.

Apa yang berubah?

Dimulai dengan Android 8.0 Oreo , grup izin TELEPON juga berisi izin ANSWER_PHONE_CALLS . Seperti yang disarankan oleh nama izin, menahannya memungkinkan aplikasi Anda menerima panggilan masuk secara terprogram melalui panggilan API yang tepat tanpa peretasan apa pun di sekitar sistem menggunakan refleksi atau simulasi pengguna.

Bagaimana kami memanfaatkan perubahan ini?

Anda harus memeriksa versi sistem pada waktu proses jika Anda mendukung versi Android yang lebih lama sehingga Anda dapat merangkum panggilan API baru ini sambil mempertahankan dukungan untuk versi Android yang lebih lama tersebut. Anda harus mengikuti permintaan izin pada waktu proses untuk mendapatkan izin baru tersebut selama waktu proses, seperti standar pada versi Android yang lebih baru.

Setelah mendapatkan izin, aplikasi Anda cukup memanggil metode acceptRingingCall TelecomManager . Doa dasar terlihat sebagai berikut:

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

Metode 1: TelephonyManager.answerRingingCall ()

Saat Anda memiliki kendali tak terbatas atas perangkat.

Apa ini?

Ada TelephonyManager.answerRingingCall () yang merupakan metode internal tersembunyi. Ini berfungsi sebagai jembatan untuk ITelephony.answerRingingCall () yang telah dibahas di interwebs dan tampaknya menjanjikan di awal. Ini tidak tersedia di 4.4.2_r1 karena diperkenalkan hanya di commit 83da75d untuk Android 4.4 KitKat ( baris 1537 di 4.4.3_r1 ) dan kemudian "diperkenalkan kembali" di commit f1e1e77 untuk Lollipop ( baris 3138 di 5.0.0_r1 ) karena cara Pohon Git terstruktur. Ini berarti bahwa kecuali Anda hanya mendukung perangkat dengan Lollipop, yang mungkin merupakan keputusan yang buruk karena kecilnya pangsa pasar saat ini, Anda masih perlu menyediakan metode fallback jika menggunakan cara ini.

Bagaimana kita menggunakan ini?

Karena metode yang dimaksud tersembunyi dari penggunaan aplikasi SDK, Anda perlu menggunakan refleksi untuk memeriksa dan menggunakan metode tersebut secara dinamis selama runtime. Jika Anda tidak terbiasa dengan refleksi, Anda dapat dengan cepat membaca Apa itu refleksi, dan mengapa itu berguna? . Anda juga dapat menggali lebih dalam secara spesifik di Trail: The Reflection API jika Anda tertarik untuk melakukannya.

Dan bagaimana itu terlihat dalam kode?

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

Ini terlalu indah untuk menjadi kenyataan!

Sebenarnya, ada satu masalah kecil. Metode ini harus berfungsi penuh, tetapi manajer keamanan ingin penelepon memegang android.permission.MODIFY_PHONE_STATE . Izin ini hanya terdapat pada fitur sistem yang terdokumentasi sebagian karena pihak ketiga tidak diharapkan untuk menyentuhnya (seperti yang Anda lihat dari dokumentasinya). Anda dapat mencoba menambahkan <uses-permission>untuk itu tetapi itu tidak akan berguna karena tingkat perlindungan untuk izin ini adalah tanda tangan | sistem ( lihat baris 1201 dari core / AndroidManifest di 5.0.0_r1 ).

Anda dapat membaca Masalah 34785: Perbarui dokumentasi android: protectionLevel yang dibuat pada tahun 2012 untuk melihat bahwa kami kehilangan detail tentang "sintaks pipa" tertentu, tetapi dari bereksperimen, tampaknya itu harus berfungsi sebagai 'DAN' yang berarti semua bendera tertentu harus dipenuhi untuk mendapatkan izin. Bekerja dengan asumsi itu, itu berarti Anda harus memiliki aplikasi Anda:

  1. Dipasang sebagai aplikasi sistem.

    Ini seharusnya baik-baik saja dan dapat dilakukan dengan meminta pengguna untuk menginstal menggunakan ZIP dalam pemulihan, seperti saat melakukan rooting atau menginstal aplikasi Google pada ROM kustom yang belum dikemas.

  2. Ditandatangani dengan tanda tangan yang sama sebagai kerangka kerja / basis alias sistem, alias ROM.

    Di sinilah masalah muncul. Untuk melakukan ini, Anda harus memiliki kunci yang digunakan untuk menandatangani framework / base. Anda tidak hanya harus mendapatkan akses ke kunci Google untuk gambar pabrik Nexus, tetapi Anda juga harus mendapatkan akses ke semua kunci pengembang OEM dan ROM lainnya. Ini tampaknya tidak masuk akal sehingga Anda dapat membuat aplikasi Anda ditandatangani dengan kunci sistem dengan membuat ROM khusus dan meminta pengguna Anda untuk beralih ke sana (yang mungkin sulit) atau dengan menemukan eksploitasi yang dapat digunakan untuk melewati tingkat perlindungan izin. (yang mungkin sulit juga).

Selain itu, perilaku ini tampaknya terkait dengan Masalah 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS tidak lagi berfungsi yang menggunakan tingkat perlindungan yang sama bersama dengan tanda pengembangan yang tidak berdokumen juga.

Bekerja dengan TelephonyManager terdengar bagus, tapi tidak akan berhasil kecuali Anda mendapatkan izin yang sesuai yang tidak mudah dilakukan dalam praktiknya.

Bagaimana dengan menggunakan TelephonyManager dengan cara lain?

Sayangnya, tampaknya Anda harus memegang izin android.permission.MODIFY_PHONE_STATE untuk menggunakan alat keren yang pada gilirannya berarti Anda akan kesulitan mendapatkan akses ke metode tersebut.


Metode 2: Panggilan Layanan KODE LAYANAN

Saat Anda dapat menguji apakah build yang berjalan di perangkat akan berfungsi dengan kode yang ditentukan.

Tanpa bisa berinteraksi dengan TelephonyManager, ada juga kemungkinan untuk berinteraksi dengan layanan melalui serviceexecutable.

Bagaimana cara kerjanya?

Ini cukup sederhana, tetapi dokumentasi tentang rute ini bahkan lebih sedikit daripada yang lain. Kami tahu pasti bahwa executable membutuhkan dua argumen - nama layanan dan kode.

  • Nama layanan yang ingin kami gunakan adalah telepon .

    Ini bisa dilihat dengan berlari service list.

  • The kode kita ingin menggunakan tampaknya telah 6 tetapi tampaknya sekarang menjadi 5 .

    Sepertinya itu telah didasarkan pada IBinder.FIRST_CALL_TRANSACTION + 5 untuk banyak versi sekarang (dari 1.5_r4 hingga 4.4.4_r1 ) tetapi selama pengujian lokal, kode 5 berfungsi untuk menjawab panggilan masuk. Karena Lollipo adalah pembaruan besar-besaran, hal itu dapat dimengerti bahwa internal juga berubah di sini.

Ini hasil dengan perintah service call phone 5.

Bagaimana kita memanfaatkan ini secara terprogram?

Jawa

Kode berikut adalah implementasi kasar yang dibuat untuk berfungsi sebagai bukti konsep. Jika Anda benar-benar ingin melanjutkan dan menggunakan metode ini, Anda mungkin ingin memeriksa pedoman untuk penggunaan su bebas masalah dan mungkin beralih ke libsuperuser yang lebih dikembangkan oleh 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();
}

Nyata

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

Apakah ini benar-benar membutuhkan akses root?

Sayangnya, sepertinya begitu. Anda dapat mencoba menggunakan Runtime.exec di atasnya, tetapi saya tidak bisa mendapatkan keberuntungan dengan rute itu.

Seberapa stabil ini?

Saya senang Anda bertanya. Karena tidak didokumentasikan, ini dapat merusak berbagai versi, seperti yang digambarkan oleh perbedaan kode yang tampak di atas. Nama layanan mungkin harus tetap telepon di berbagai build, tetapi untuk semua yang kita tahu, nilai kode dapat berubah di beberapa build dari versi yang sama (modifikasi internal oleh, katakanlah, skin OEM) pada gilirannya melanggar metode yang digunakan. Oleh karena itu perlu disebutkan pengujian dilakukan pada Nexus 4 (mako / occam). Saya pribadi menyarankan Anda untuk tidak menggunakan metode ini, tetapi karena saya tidak dapat menemukan metode yang lebih stabil, saya yakin ini adalah bidikan terbaik.


Metode asli: Maksud kode tombol headset

Saat-saat ketika Anda harus puas.

Bagian berikut sangat dipengaruhi oleh jawaban ini oleh Riley C .

Metode maksud headset yang disimulasikan seperti yang diposting dalam pertanyaan asli tampaknya disiarkan seperti yang diharapkan, tetapi tampaknya tidak mencapai tujuan untuk menjawab panggilan tersebut. Meskipun tampaknya ada kode di tempat yang harus menangani maksud tersebut, mereka sama sekali tidak diperhatikan, yang berarti harus ada semacam tindakan penanggulangan baru yang diterapkan terhadap metode ini. Log juga tidak menunjukkan apa pun yang menarik dan saya pribadi tidak percaya menggali melalui sumber Android untuk ini akan bermanfaat hanya karena kemungkinan Google memperkenalkan sedikit perubahan yang dengan mudah merusak metode yang digunakan.

Adakah yang bisa kami lakukan sekarang?

Perilaku tersebut dapat direproduksi secara konsisten menggunakan input yang dapat dieksekusi. Dibutuhkan dalam argumen kode kunci, yang hanya kita berikan di KeyEvent.KEYCODE_HEADSETHOOK . Metode ini bahkan tidak memerlukan akses root sehingga cocok untuk kasus penggunaan umum di masyarakat umum, tetapi ada sedikit kekurangan dalam metode ini - acara penekanan tombol headset tidak dapat ditentukan untuk memerlukan izin, artinya ini berfungsi seperti aslinya. tekan tombol dan menggelembung melalui seluruh rantai, yang pada gilirannya berarti Anda harus berhati-hati tentang kapan harus mensimulasikan penekanan tombol karena bisa, misalnya, memicu pemutar musik untuk memulai pemutaran jika tidak ada orang lain dengan prioritas lebih tinggi yang siap untuk menangani acara.

Kode?

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

Ada API publik yang bagus untuk Android 8.0 Oreo dan yang lebih baru.

Tidak ada API publik sebelum Android 8.0 Oreo. API internal terlarang atau tanpa dokumentasi. Anda harus melanjutkan dengan hati-hati.

Valter Jansons
sumber
Berkenaan dengan maksud kode kunci headset, sudahkah Anda memeriksa sumber Google di sini untuk alasan apa pun mengapa mereka berhenti ditindaklanjuti? Lucunya, panggilan masih dapat dengan mudah ditolak dengan maksud ini (cukup tiru tekan lama), tetapi tidak ada yang berhasil menjawab. Saya belum menemukan pemeriksaan izin eksplisit atau blok potensial lainnya dan saya berharap pandangan kedua mungkin mengungkap sesuatu.
Riley C
Agak sibuk maka penundaan - akan mencoba meluangkan waktu untuk mencari tahu. Setelah melihat sekilas, tampaknya CallsManager membuat HeadsetMediaButton. Callback sesi di sana harus menangani pemanggilan handleHeadsetHook (KeyEvent) setelah callback dari MediaSessionManager. Semua kode tampaknya cocok ... tetapi saya bertanya-tanya, dapatkah seseorang menghapus niat KeyEvent.ACTION_DOWN untuk menguji? (Artinya, hanya aktifkan KeyEvent.ACTION_UP satu kali.)
Valter Jansons
Sebenarnya, HeadsetMediaButton bekerja dengan MediaSession dan tidak berinteraksi dengan MediaSessionManager secara langsung ...
Valter Jansons
1
Bagi Anda yang berhasil menemukan situasi di mana Metode Asli tampaknya tidak berfungsi (misalnya Lollipop memiliki masalah), saya punya kabar baik dan buruk: Saya telah berhasil membuat ACTION_UP berfungsi 100%, dalam kode saya, dengan FULL_WAKE_LOCK. Ini tidak akan bekerja dengan PARTIAL_WAKE_LOCK. Sama sekali tidak ada dokumentasi apa pun tentang mengapa ini terjadi. Saya akan merinci ini di jawaban mendatang ketika saya menguji kode eksperimen saya secara lebih ekstensif. Kabar buruknya, tentu saja FULL_WAKE_LOCK sudah tidak digunakan lagi, jadi ini adalah perbaikan yang hanya akan bertahan selama Google menyimpannya di API.
leRobot
1
Tangkapan dari jawaban asli tidak disebut dalam banyak kasus. Saya merasa lebih baik memanggil exec terlebih dahulu dan kemudian memanggil tombol ke atas segera setelahnya.
Warpzit
36

Solusi yang berfungsi sepenuhnya didasarkan pada kode @Valter Strods.

Untuk membuatnya berfungsi, Anda harus menampilkan aktivitas (tidak terlihat) di layar kunci tempat kode dijalankan.

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>

Panggilan Terima Aktivitas

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

Gaya

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

Akhirnya panggil keajaiban!

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
sumber
1
di mana mi suppoz untuk menambahkan kode di bawah "Akhirnya memanggil keajaiban". Apakah ini akan berfungsi untuk Android 6.0
Akshay Shah
Saya datang ke sini untuk mengatakan bahwa broadcastHeadsetConnected (boolean connected) adalah yang memecahkan masalah di perangkat Samsung A3 2016. Tanpa itu, metode yang sangat mirip (menggunakan aktivitas terpisah, transparan, dan utas untuk pemanggilan dan yang lainnya) berfungsi penuh untuk sekitar 20 perangkat yang diuji, kemudian A3 ini muncul dan memaksa saya untuk memeriksa ulang pertanyaan ini untuk jawaban baru. Setelah membandingkan dengan kode saya, itulah perbedaan yang signifikan!
leRobot
1
Bagaimana saya bisa menolak panggilan juga? Bisakah Anda memperbarui jawaban untuk menunjukkan ini?
Amanni
@leRobot Jawaban ini memeriksa apakah itu perangkat HTC untuk broadcastHeadsetConnected, bagaimana Anda dapat memeriksa apakah itu perangkat Samsung A3 2016? Ngomong-ngomong, ini benar-benar jawaban yang bagus, aplikasi saya dapat menjawab panggilan telepon bahkan layarnya terkunci.
eepty
@eepty Anda dapat menggunakan referensi perangkat resmi untuk data Build. ( support.google.com/googleplay/answer/1727131?hl=id ). Saya menggunakan Build.MODEL.startsWith ("SM-A310") untuk A3 2016. TAPI! Saya dapat mengonfirmasi bahwa A3 2016 tidak mendukung siaran yang terhubung dengan headset! Apa yang sebenarnya memecahkan masalah saya adalah mengubah urutan sehingga Runtime.getRuntime (). Exec (... terpicu pertama kali untuk perangkat ini. Tampaknya berfungsi setiap saat untuk perangkat itu dan tidak kembali ke pengecualian.
leRobot
14

Berikut ini adalah pendekatan alternatif yang berhasil bagi saya. Ini mengirimkan peristiwa kunci ke server telekomunikasi secara langsung menggunakan MediaController API. Ini mengharuskan aplikasi memiliki izin BIND_NOTIFICATION_LISTENER_SERVICE dan diberikan secara eksplisit akses Notifikasi dari pengguna:

@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 pada kode diatas bisa jadi hanya kelas kosong.

import android.service.notification.NotificationListenerService;

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

Dengan bagian yang sesuai di manifes:

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

Karena target acara eksplisit, ini mungkin harus menghindari efek samping apa pun yang memicu pemutar media.

Catatan: server telekomunikasi mungkin tidak langsung aktif setelah peristiwa dering. Agar ini bekerja dengan andal, mungkin berguna bagi aplikasi untuk mengimplementasikan MediaSessionManager.OnActiveSessionsChangedListener untuk memantau kapan server telekomunikasi menjadi aktif, sebelum mengirim peristiwa.

Memperbarui:

Di Android O , seseorang perlu melakukan simulasi ACTION_DOWNsebelumnya ACTION_UP, jika tidak, hal di atas tidak akan berpengaruh. yaitu yang berikut ini diperlukan:

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

Tetapi karena panggilan resmi untuk menjawab panggilan tersedia sejak Android O (lihat jawaban teratas), mungkin tidak diperlukan lagi peretasan ini, kecuali seseorang terjebak dengan tingkat API kompilasi lama sebelum Android O.

headuck
sumber
Itu tidak berhasil untuk saya. Ini mengembalikan kesalahan Izin. Akses ke notifikasi tidak diberikan ke aplikasi. Saya menggunakan Android L
Jame
2
Ini membutuhkan langkah tambahan untuk secara eksplisit memberikan izin oleh pengguna, di suatu tempat di menu pengaturan bergantung pada sistem, selain menerima izin dalam manifes.
headuck
melaporkan ini tampaknya berfungsi dalam kasus sempit: Galaxy A3 2016 dengan Marshmallow. Saya akan menguji ini pada sekelompok perangkat A3 yang tidak bekerja dengan metode input keyevent karena FATAL EXCEPTION: java.lang.SecurityException: Menyuntikkan ke aplikasi lain memerlukan izin INJECT_EVENTS. Perangkat yang menyinggung adalah sekitar 2% dari basis pengguna saya dan saya tidak mereplikasi pengecualian mereka, tetapi akan mencoba metode ini untuk melihat apakah mereka berhasil menerima panggilan. Untungnya aplikasi saya sudah meminta notif eksplisit. akses untuk tujuan lain.
leRobot
setelah pengujian ekstensif, dengan senang hati saya melaporkan bahwa perangkat A3 2016 yang gagal dengan "input keyevent" exec berhasil bekerja dengan metode MediaController # dispatchMediaButtonEvent (<hook KeryEvent>)). ini jelas hanya berfungsi setelah pengguna mengizinkan Akses Pemberitahuan eksplisit, jadi Anda harus menambahkan layar yang mengarahkan ke Pengaturan Android untuk itu, dan pada dasarnya Anda membutuhkan pengguna untuk mengambil tindakan ekstra untuk ini, seperti yang dijelaskan pada jawabannya. Di aplikasi saya, kami telah mengambil langkah ekstra untuk terus menanyakan ini jika pengguna membuka layar itu tetapi tidak menambahkan notif. akses
leRobot
Ini berfungsi di Android Nougat. Solusi @notz berfungsi dengan baik jika tidak, tetapi mengeluh "Hanya sistem yang dapat mengirimkan peristiwa kunci media ke sesi prioritas global" di Android 7.
PB
9

Untuk menguraikan sedikit jawaban oleh @Muzikant, dan memodifikasinya sedikit agar bekerja sedikit lebih bersih di perangkat saya, coba input keyevent 79, konstanta untuk KeyEvent.KEYCODE_HEADSETHOOK . Sangat kasar:

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

Maafkan konvensi pengkodean yang cukup buruk, saya tidak terlalu berpengalaman dalam panggilan Runtime.exec (). Perhatikan bahwa perangkat saya tidak di-root, dan saya juga tidak meminta hak akses root.

Masalah dengan pendekatan ini adalah bahwa itu hanya berfungsi dalam kondisi tertentu (untuk saya). Artinya, jika saya menjalankan utas di atas dari opsi menu yang dipilih pengguna saat panggilan berdering, panggilan akan menjawab dengan baik. Jika saya menjalankannya dari penerima yang memantau status panggilan masuk, itu akan benar-benar diabaikan.

Jadi pada Nexus 5 saya, ini berfungsi dengan baik untuk jawaban yang digerakkan oleh pengguna dan harus sesuai dengan tujuan layar panggilan khusus. Itu tidak akan berfungsi untuk semua jenis aplikasi jenis kontrol panggilan otomatis.

Yang juga perlu diperhatikan adalah semua kemungkinan peringatan, termasuk bahwa ini juga mungkin akan berhenti berfungsi dalam satu atau dua pembaruan.

Riley C
sumber
input keyevent 79berfungsi dengan baik di Sony Xperia 5.0. Berfungsi saat menelepon dari suatu aktivitas atau dari penerima siaran.
nicolas
0

melalui perintah adb Cara menerima panggilan oleh adb

Perlu diingat bahwa Android adalah Linux dengan JVM besar di bagian depan. Anda dapat mengunduh aplikasi baris perintah dan melakukan root pada ponsel dan sekarang Anda memiliki komputer Linux biasa dan baris perintah yang melakukan semua hal normal. Jalankan skrip, Anda bahkan dapat ssh untuk itu (trik OpenVPN)

pengguna1544207
sumber
0

Terima kasih @notz jawabannya bekerja untuk saya di Lolillop. Agar kode ini tetap berfungsi dengan SDK Android lama, Anda dapat melakukan kode ini:

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

Cara mengaktifkan Speaker Telepon setelah menjawab panggilan secara otomatis.

Saya menyelesaikan masalah saya di atas dengan setSpeakerphoneOn. Saya pikir ini layak untuk diposting di sini, karena kasus penggunaan untuk menjawab panggilan telepon secara otomatis seringkali juga membutuhkan speakerphone agar berguna. Terima kasih sekali lagi untuk semua orang di utas ini, pekerjaan yang luar biasa.

Ini berfungsi untuk saya di Android 5.1.1 di Nexus 4 saya tanpa ROOT. ;)

Izin diperlukan:

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

Kode 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
}
Madhava Jay
sumber
1
Menarik. Saya sebenarnya mencoba menjawab panggilan dan menyalakan speaker bersama sehingga pendekatan ini sepertinya menyelesaikan keduanya :). Saya memiliki pertanyaan serupa seperti beberapa komentar di jawaban lain: ke mana perginya kode ini?
fangmobile
-1

Jalankan perintah berikut sebagai root:

input keyevent 5

Detail selengkapnya tentang simulasi peristiwa penting di sini .

Anda dapat menggunakan kelas dasar yang saya buat ini untuk menjalankan perintah sebagai root dari aplikasi Anda.

Muzikant
sumber
1
Saat menguji dengan profil pengguna biasa, ini memunculkan UI dalam panggilan untuk saya, meminta saya untuk menggesek ke kiri / kanan untuk menolak / menjawab atau menggunakan tindakan / tanggapan cepat. Jika OP membuat layar panggilan masuk khusus , ini tidak benar-benar membantu kecuali jika berperilaku berbeda di bawah root, yang saya ragu seolah-olah itu tidak berperilaku baik untuk pengguna biasa, panggilan mungkin akan gagal dan tidak. memicu tindakan yang berbeda.
Valter Jansons
-2

uji ini: pertama tambahkan izin lalu gunakan killCall () untuk menutup, gunakan answerCall () untuk menjawab panggilan

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

FYI jika Anda tertarik pada cara MENGAKHIRI panggilan yang sedang berlangsung di Android O, Valter Method 1: TelephonyManager.answerRingingCall()bekerja jika Anda mengubah metode yang Anda panggil endCall.

Ini hanya membutuhkan android.permission.CALL_PHONEizin.

Berikut kodenya:

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