Làm cách nào để có thể trả lời các cuộc gọi đến theo chương trình trong Android 5.0 (Lollipop)?


87

Khi tôi đang cố gắng tạo màn hình tùy chỉnh cho các cuộc gọi đến, tôi đang cố gắng trả lời cuộc gọi đến theo chương trình. Tôi đang sử dụng mã sau đây nhưng nó không hoạt động trong 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 Man, tại sao lại chạy vào điều này, chỉ cần trượt Man! có vẻ dễ dàng hơn với tôi \ m /
nobalG

Tôi đang tạo màn hình cuộc gọi đến tùy chỉnh cho người dùng Android.
maveroid

2
Bất kỳ ai? Tôi cũng quan tâm đến cái này! Đã thử rất nhiều thứ, nhưng chúng không thành công: /
Arthur

1
@nobalG anh ấy đang nói theo chương trình
dsharew

1
@maveroid, Bạn đã nghĩ ra giải pháp cho Android 5.0 chưa?
arthursfreire

Câu trả lời:


155

Cập nhật với Android 8.0 Oreo

Mặc dù câu hỏi ban đầu được đặt ra là hỗ trợ Android L, nhưng mọi người dường như vẫn đang đánh vào câu hỏi và câu trả lời này, vì vậy cần mô tả những cải tiến được giới thiệu trong Android 8.0 Oreo. Các phương pháp tương thích ngược vẫn được mô tả bên dưới.

Những gì đã thay đổi?

Bắt đầu với Android 8.0 Oreo , nhóm quyền PHONE cũng chứa quyền ANSWER_PHONE_CALLS . Như tên của quyền cho thấy, việc giữ nó cho phép ứng dụng của bạn chấp nhận các cuộc gọi đến theo chương trình thông qua một lệnh gọi API thích hợp mà không có bất kỳ hành vi tấn công nào xung quanh hệ thống bằng cách sử dụng phản chiếu hoặc mô phỏng người dùng.

Làm thế nào để chúng tôi sử dụng thay đổi này?

Bạn nên kiểm tra phiên bản hệ thống trong thời gian chạy nếu bạn đang hỗ trợ các phiên bản Android cũ hơn để có thể đóng gói lệnh gọi API mới này trong khi vẫn duy trì hỗ trợ cho các phiên bản Android cũ hơn đó. Bạn nên tuân theo yêu cầu quyền tại thời điểm chạy để có được quyền mới đó trong thời gian chạy, như tiêu chuẩn trên các phiên bản Android mới hơn.

Sau khi có được quyền, ứng dụng của bạn chỉ cần gọi phương thức acceptRingingCall của TelecomManager . Một lời gọi cơ bản trông như sau:

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

Phương pháp 1: TelephonyManager.answerRingingCall ()

Đối với khi bạn có quyền kiểm soát không giới hạn đối với thiết bị.

Cái này là cái gì?

Có TelephonyManager.answerRingingCall () là một phương thức ẩn, nội bộ. Nó hoạt động như một cầu nối cho ITelephony.answerRingingCall () đã được thảo luận trên các mạng interwebs và có vẻ đầy hứa hẹn khi bắt đầu. Nó không có sẵn trên 4.4.2_r1 vì nó chỉ được giới thiệu trong cam kết 83da75d cho Android 4.4 KitKat ( dòng 1537 trên 4.4.3_r1 ) và sau đó được "giới thiệu lại" trong cam kết f1e1e77 cho Lollipop ( dòng 3138 trên 5.0.0_r1 ) do cách Git tree đã được cấu trúc. Điều này có nghĩa là trừ khi bạn chỉ hỗ trợ các thiết bị có Lollipop, đây có thể là một quyết định tồi dựa trên thị phần nhỏ của nó tính đến thời điểm hiện tại, bạn vẫn cần cung cấp các phương pháp dự phòng nếu đi theo con đường này.

Làm thế nào chúng ta sẽ sử dụng điều này?

Vì phương pháp được đề cập bị ẩn khỏi việc sử dụng các ứng dụng SDK, bạn cần sử dụng phản chiếu để kiểm tra động và sử dụng phương pháp trong thời gian chạy. Nếu bạn không quen thuộc với phản xạ, bạn có thể đọc nhanh Phản ánh là gì, và tại sao nó lại hữu ích? . Bạn cũng có thể tìm hiểu sâu hơn về các chi tiết cụ thể tại Trail: API Reflection nếu bạn muốn làm như vậy.

Và nó trông như thế nào trong mã?

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

Điều này là quá tốt là đúng!

Trên thực tế, có một vấn đề nhỏ. Phương thức này phải có đầy đủ chức năng, nhưng người quản lý bảo mật muốn người gọi giữ android.permission.MODIFY_PHONE_STATE . Quyền này chỉ nằm trong lĩnh vực của các tính năng được ghi lại một phần của hệ thống vì các bên thứ 3 không được phép chạm vào nó (như bạn có thể thấy từ tài liệu cho nó). Bạn có thể thử thêm một <uses-permission>cho nó nhưng điều đó sẽ không hiệu quả vì mức độ bảo vệ cho quyền này là hệ thống chữ ký | ( xem dòng 1201 của core / AndroidManifest trên 5.0.0_r1 ).

Bạn có thể đọc Vấn đề 34785: Cập nhật tài liệu android: protectionLevel được tạo vào năm 2012 để thấy rằng chúng tôi còn thiếu thông tin chi tiết về "cú pháp ống" cụ thể, nhưng từ thử nghiệm xung quanh, nó có vẻ như nó phải hoạt động như một 'VÀ' nghĩa là tất cả các cờ cụ thể phải được thực hiện để được cấp quyền. Làm việc theo giả định đó, có nghĩa là bạn phải có ứng dụng của mình:

  1. Được cài đặt như một ứng dụng hệ thống.

    Điều này sẽ ổn và có thể được thực hiện bằng cách yêu cầu người dùng cài đặt bằng cách sử dụng ZIP trong khôi phục, chẳng hạn như khi root hoặc cài đặt ứng dụng Google trên ROM tùy chỉnh chưa được đóng gói sẵn.

  2. Được ký bằng chữ ký giống như các khung / cơ sở hay còn gọi là hệ thống, hay còn gọi là ROM.

    Đây là nơi mà các vấn đề xuất hiện. Để làm được điều này, bạn cần nắm chắc các phím được sử dụng để ký khung / cơ sở. Bạn không chỉ phải có quyền truy cập vào các khóa của Google dành cho hình ảnh xuất xưởng của Nexus mà còn phải có quyền truy cập vào tất cả các khóa của nhà phát triển ROM và OEM khác. Điều này có vẻ không hợp lý vì vậy bạn có thể ký ứng dụng của mình bằng các khóa hệ thống bằng cách tạo một ROM tùy chỉnh và yêu cầu người dùng của bạn chuyển sang nó (điều này có thể khó) hoặc bằng cách tìm một cách khai thác mà mức độ bảo vệ quyền có thể bị bỏ qua (cũng có thể khó).

Ngoài ra, hành vi này dường như liên quan đến Vấn đề 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS không còn hoạt động sử dụng cùng mức bảo vệ cùng với cờ phát triển không có tài liệu.

Làm việc với TelephonyManager nghe có vẻ tốt, nhưng sẽ không hoạt động trừ khi bạn nhận được sự cho phép thích hợp, điều không dễ thực hiện trong thực tế.

Điều gì về việc sử dụng TelephonyManager theo những cách khác?

Đáng buồn thay, có vẻ như yêu cầu bạn phải giữ android.permission.MODIFY_PHONE_STATE để sử dụng các công cụ thú vị, điều này có nghĩa là bạn sẽ gặp khó khăn khi truy cập vào các phương pháp đó.


Cách 2: gọi dịch vụ MÃ DỊCH VỤ

Để biết khi nào bạn có thể kiểm tra rằng bản dựng đang chạy trên thiết bị sẽ hoạt động với mã được chỉ định.

Nếu không có khả năng tương tác với TelephonyManager, cũng có khả năng tương tác với dịch vụ thông qua servicetệp thực thi.

Cái này hoạt động ra sao?

Nó khá đơn giản, nhưng thậm chí có ít tài liệu hơn về tuyến đường này so với những tuyến đường khác. Chúng tôi biết chắc chắn tệp thực thi có hai đối số - tên dịch vụ và mã.

  • Tên dịch vụ chúng tôi muốn sử dụng là điện thoại .

    Điều này có thể được nhìn thấy bằng cách chạy service list.

  • Các đang chúng ta muốn sử dụng dường như đã được 6 nhưng dường như đến nay được 5 .

    Có vẻ như nó đã được dựa trên IBinder.FIRST_CALL_TRANSACTION + 5 cho nhiều phiên bản hiện nay (từ 1.5_r4 đến 4.4.4_r1 ) nhưng trong quá trình thử nghiệm cục bộ, mã 5 đã hoạt động để trả lời cuộc gọi đến. Vì Lollipo là một bản cập nhật lớn, nên có thể hiểu được nội dung bên trong cũng thay đổi ở đây.

Điều này dẫn đến một lệnh của service call phone 5.

Làm thế nào để chúng tôi sử dụng điều này theo chương trình?

Java

Đoạn mã sau là một triển khai thô được thực hiện để hoạt động như một bằng chứng về khái niệm. Nếu bạn thực sự muốn tiếp tục và sử dụng phương pháp này, bạn có thể muốn kiểm tra hướng dẫn sử dụng su không gặp sự cố và có thể chuyển sang libsuperuser được phát triển đầy đủ hơn bởi 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();
}

Rõ ràng

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

Điều này có thực sự yêu cầu quyền truy cập root không?

Đáng buồn thay, nó có vẻ như vậy. Bạn có thể thử sử dụng Runtime.exec trên đó, nhưng tôi không thể gặp may với lộ trình đó.

Làm thế nào ổn định là điều này?

Tôi rất vui vì bạn đã hỏi. Do không được ghi chép lại, điều này có thể xảy ra trên nhiều phiên bản khác nhau, như được minh họa bằng sự khác biệt về mã có vẻ ở trên. Tên dịch vụ có lẽ nên ở trên điện thoại trên các bản dựng khác nhau, nhưng đối với tất cả những gì chúng ta biết, giá trị mã có thể thay đổi trên nhiều bản dựng của cùng một phiên bản (sửa đổi nội bộ bằng cách ví dụ, giao diện của OEM) lần lượt phá vỡ phương pháp được sử dụng. Do đó, điều đáng nói là thử nghiệm diễn ra trên Nexus 4 (mako / Occam). Cá nhân tôi khuyên bạn không nên sử dụng phương pháp này, nhưng vì tôi không thể tìm được phương pháp ổn định hơn, tôi tin rằng đây là cách tốt nhất.


Phương pháp ban đầu: Ý định mã khóa tai nghe

Cho những lúc bạn phải giải quyết.

Phần sau bị ảnh hưởng mạnh mẽ bởi câu trả lời này bởi Riley C .

Phương pháp ý định tai nghe mô phỏng như được đăng trong câu hỏi ban đầu dường như được phát sóng giống như người ta mong đợi, nhưng nó dường như không đạt được mục tiêu trả lời cuộc gọi. Mặc dù dường như có mã sẵn sàng xử lý những ý định đó, nhưng chúng chỉ đơn giản là không được quan tâm, điều đó có nghĩa là phải có một số loại biện pháp đối phó mới để chống lại phương pháp này. Nhật ký cũng không hiển thị bất kỳ điều gì đáng quan tâm và cá nhân tôi không tin rằng việc đào qua nguồn Android cho điều này sẽ đáng giá chỉ do khả năng Google giới thiệu một thay đổi nhỏ dễ dàng phá vỡ phương pháp được sử dụng.

Có điều gì chúng ta có thể làm ngay bây giờ không?

Hành vi có thể được tái tạo nhất quán bằng cách sử dụng tệp thực thi đầu vào. Nó có một đối số mã khóa, mà chúng ta chỉ cần truyền vào KeyEvent.KEYCODE_HEADSETHOOK . Phương pháp này thậm chí không yêu cầu quyền truy cập root làm cho nó phù hợp với các trường hợp sử dụng phổ biến trong công chúng, nhưng có một nhược điểm nhỏ trong phương pháp - sự kiện nhấn nút tai nghe không thể được chỉ định để yêu cầu sự cho phép, có nghĩa là nó hoạt động như thật nhấn nút và bong bóng lên trong toàn bộ chuỗi, điều này có nghĩa là bạn phải thận trọng về thời điểm mô phỏng thao tác nhấn nút có thể, ví dụ: kích hoạt trình phát nhạc bắt đầu phát lại nếu không có ai khác có mức độ ưu tiên cao hơn sẵn sàng xử lý sự kiện.

Mã?

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

Có một API công khai tuyệt vời cho Android 8.0 Oreo trở lên.

Không có API công khai trước Android 8.0 Oreo. Các API nội bộ nằm ngoài giới hạn hoặc đơn giản là không có tài liệu. Bạn nên tiến hành một cách thận trọng.


Liên quan đến ý định mã khóa tai nghe, bạn đã kiểm tra nguồn Google tại đây để biết lý do tại sao chúng không bị xử lý? Điều buồn cười là các cuộc gọi vẫn có thể dễ dàng bị từ chối với những ý định này (chỉ cần giả lập một thao tác nhấn và giữ), nhưng không có gì hoạt động để trả lời. Tôi vẫn chưa tìm thấy kiểm tra quyền rõ ràng hoặc khối tiềm năng khác và tôi hy vọng bộ mắt thứ hai có thể khám phá ra điều gì đó.
Riley C

Có một chút bận rộn nên sự chậm trễ - sẽ cố gắng dành một chút thời gian để tìm ra điều này. Sau khi xem xét nhanh, có vẻ như CallsManager xây dựng HeadsetMediaButton. Cuộc gọi lại phiên ở đó sẽ quan tâm đến việc gọi handleHeadsetHook (KeyEvent) khi gọi lại từ MediaSessionManager. Tất cả mã dường như khớp với nhau ... nhưng tôi tự hỏi, ai đó có thể xóa ý định KeyEvent.ACTION_DOWN để kiểm tra không? (Nghĩa là, chỉ kích hoạt KeyEvent.ACTION_UP một lần duy nhất.)
Valter Jansons

Trên thực tế, HeadsetMediaButton làm việc với các MediaSession và không tương tác với MediaSessionManager trực tiếp ...
Valter Jansons

1
Đối với những người trong số các bạn đã cố gắng tìm thấy tình huống mà Phương pháp gốc dường như không hoạt động (ví dụ: Lollipop có vấn đề), tôi có tin tốt và xấu: Tôi đã quản lý để ACTION_UP hoạt động 100%, trong mã của tôi, với FULL_WAKE_LOCK. Nó sẽ không hoạt động với PARTIAL_WAKE_LOCK. Hoàn toàn không có bất kỳ tài liệu nào về lý do tại sao của điều này. Tôi sẽ trình bày chi tiết điều này trong câu trả lời trong tương lai khi tôi kiểm tra mã thử nghiệm của mình rộng rãi hơn. Tất nhiên, tin xấu là FULL_WAKE_LOCK không được dùng nữa, vì vậy đây là bản sửa lỗi sẽ chỉ tồn tại miễn là Google giữ nó trong API.
leRobot

1
Phần bắt từ câu trả lời ban đầu không được gọi trong nhiều trường hợp. Tôi thấy tốt hơn là chỉ cần gọi thực thi trước và sau đó vẫn gọi nút lên ngay sau đó.
Warpzit

36

Giải pháp hoạt động đầy đủ dựa trên mã @Valter Strods.

Để nó hoạt động, bạn phải hiển thị một hoạt động (ẩn) trên màn hình khóa nơi mã được thực thi.

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>

Hoạt động chấp nhận cuộc gọi

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

Phong cách

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

Cuối cùng gọi là kỳ diệu!

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
nơi mi suppoz để thêm mã dưới "Cuối cùng gọi điều kỳ diệu". Nó sẽ hoạt động cho Android 6.0
Akshay Shah

Tôi đến đây để nói rằng broadcastHeadsetConnected (kết nối boolean) là thứ đã giải quyết được sự cố trong thiết bị Samsung A3 2016. Không có điều đó, một phương pháp tương tự (sử dụng hoạt động riêng biệt, minh bạch và các chuỗi cho các lệnh gọi và nội dung không) đã hoạt động hoàn toàn cho khoảng 20 thiết bị được thử nghiệm, sau đó chiếc A3 này xuất hiện và buộc tôi phải kiểm tra lại câu hỏi này để tìm câu trả lời mới. Sau khi so sánh với mã của tôi, đó là sự khác biệt đáng kể!
leRobot

1
Tôi cũng có thể từ chối cuộc gọi như thế nào? Bạn có thể cập nhật câu trả lời để hiển thị điều này?
Amanni

@leRobot Câu trả lời này kiểm tra xem đó có phải là thiết bị HTC để broadcastHeadsetConnected hay không, làm cách nào bạn có thể kiểm tra xem đó có phải là thiết bị Samsung A3 2016 không? Nhân tiện đây thực sự là một câu trả lời hay, ứng dụng của tôi có thể trả lời cuộc gọi điện thoại ngay cả khi màn hình bị khóa.
eepty

@eepty Bạn có thể sử dụng tham chiếu thiết bị chính thức cho dữ liệu Xây dựng. ( support.google.com/googleplay/answer/1727131?hl=vi ). Tôi sử dụng Build.MODEL.startsWith ("SM-A310") cho A3 2016. NHƯNG! Tôi có thể xác nhận A3 2016 không hỗ trợ phát sóng kết nối tai nghe! Điều thực sự giải quyết được vấn đề của tôi là thay đổi thứ tự để Runtime.getRuntime (). Execute (... kích hoạt đầu tiên cho các thiết bị này. Nó dường như hoạt động mọi lúc cho thiết bị đó và không rơi vào trường hợp ngoại lệ.
leRobot

14

Sau đây là một cách tiếp cận thay thế phù hợp với tôi. Nó gửi trực tiếp sự kiện quan trọng đến máy chủ viễn thông bằng API MediaController. Điều này yêu cầu ứng dụng có quyền BIND_NOTIFICATION_LISTENER_SERVICE được người dùng cấp rõ ràng quyền truy cập Thông báo:

@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 trong đoạn mã trên có thể chỉ là một lớp trống.

import android.service.notification.NotificationListenerService;

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

Với phần tương ứng trong tệp kê khai:

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

Vì mục tiêu của sự kiện là rõ ràng, điều này có thể tránh mọi tác dụng phụ của việc kích hoạt trình phát đa phương tiện.

Lưu ý: máy chủ viễn thông có thể không hoạt động ngay sau sự kiện đổ chuông. Để điều này hoạt động đáng tin cậy, ứng dụng có thể hữu ích khi triển khai MediaSessionManager.OnActiveSessionsChangedListener để theo dõi khi máy chủ viễn thông hoạt động, trước khi gửi sự kiện.

Cập nhật:

Trong Android O , người ta cần phải mô phỏng ACTION_DOWNtrước ACTION_UP, nếu không những điều trên không có tác dụng. tức là những điều sau đây là cần thiết:

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

Nhưng vì cuộc gọi trả lời cuộc gọi chính thức có sẵn kể từ Android O (xem câu trả lời trên cùng), có thể không còn cần phải hack này nữa, trừ khi một người bị mắc kẹt với cấp API biên dịch cũ trước Android O.


Nó không làm việc cho tôi. Nó trả về lỗi Quyền. Quyền truy cập vào thông báo không được cấp cho ứng dụng. Tôi đang sử dụng Android L
Jame

2
Điều này yêu cầu một bước bổ sung là cấp quyền rõ ràng cho người dùng, ở đâu đó trong menu cài đặt tùy thuộc vào hệ thống, bên cạnh việc chấp nhận quyền trong tệp kê khai.
headuck

báo cáo điều này dường như hoạt động trong một trường hợp hẹp: Galaxy A3 2016 với Marshmallow. Tôi sẽ kiểm tra điều này trong một nhóm thiết bị A3 không hoạt động với phương thức keyevent đầu vào do ngoại lệ FATAL: java.lang.SecurityException: Việc đưa vào ứng dụng khác yêu cầu quyền INJECT_EVENTS. Các thiết bị vi phạm chiếm khoảng 2% cơ sở người dùng của tôi và tôi không tái tạo ngoại lệ của chúng, nhưng sẽ thử phương pháp này để xem liệu chúng có quản lý để nhận cuộc gọi hay không. May mắn thay, ứng dụng của tôi đã yêu cầu thông báo rõ ràng. truy cập cho các mục đích khác.
leRobot

sau khi thử nghiệm rộng rãi, tôi vui mừng thông báo rằng các thiết bị A3 2016 không thành công với phương thức "input keyevent" thực thi đã hoạt động với phương thức MediaController # sendMediaButtonEvent (<hook KeryEvent>)). điều này rõ ràng chỉ hoạt động sau khi người dùng cho phép Quyền truy cập thông báo rõ ràng, vì vậy bạn sẽ phải thêm màn hình chuyển hướng đến Cài đặt Android cho điều đó và về cơ bản bạn cần người dùng thực hiện thêm hành động cho việc này, như đã nêu chi tiết trong câu trả lời. Trong ứng dụng của tôi, chúng tôi đã thực hiện thêm các bước để tiếp tục hỏi điều này nếu người dùng truy cập vào màn hình đó nhưng không thêm notif. truy cập
leRobot

Điều này hoạt động trên Android Nougat. Giải pháp của @notz hoạt động tuyệt vời nếu không, nhưng lại phàn nàn "Chỉ hệ thống mới có thể gửi sự kiện khóa phương tiện đến phiên ưu tiên toàn cầu" trên Android 7.
Peng Bai

9

Để giải thích một chút về câu trả lời của @Muzikant và để sửa đổi nó một chút để hoạt động sạch hơn một chút trên thiết bị của tôi, hãy thử input keyevent 79, hằng số cho KeyEvent.KEYCODE_HEADSETHOOK . Rất đại khái:

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

Hãy tha thứ cho các quy ước mã hóa khá tệ, tôi không quá thành thạo về các lệnh gọi Runtime.exec (). Lưu ý rằng thiết bị của tôi chưa được root và tôi cũng không yêu cầu quyền root.

Vấn đề với cách tiếp cận này là nó chỉ hoạt động trong những điều kiện nhất định (đối với tôi). Nghĩa là, nếu tôi chạy chuỗi trên từ một tùy chọn menu mà người dùng chọn trong khi cuộc gọi đang đổ chuông, cuộc gọi sẽ trả lời tốt. Nếu tôi chạy nó từ một bộ thu theo dõi trạng thái cuộc gọi đến, nó hoàn toàn bị bỏ qua.

Vì vậy, trên Nexus 5 của tôi, nó hoạt động tốt cho trả lời theo hướng người dùng và phải phù hợp với mục đích của màn hình cuộc gọi tùy chỉnh. Nó chỉ không hoạt động đối với bất kỳ loại ứng dụng kiểu điều khiển cuộc gọi tự động nào.

Cũng cần lưu ý là tất cả các cảnh báo có thể xảy ra, bao gồm cả điều này, có thể sẽ ngừng hoạt động trong một hoặc hai bản cập nhật.


input keyevent 79hoạt động tốt trên Sony Xperia 5.0. Hoạt động khi gọi từ một hoạt động hoặc từ bộ thu phát sóng.
nicolas,

0

thông qua các lệnh của adb Cách nhận cuộc gọi bằng adb

Hãy nhớ rằng Android là Linux với một JVM lớn trên giao diện người dùng. Bạn có thể tải xuống ứng dụng dòng lệnh và root điện thoại và bây giờ bạn có một máy tính Linux thông thường và dòng lệnh thực hiện tất cả những việc bình thường. Chạy các tập lệnh, bạn thậm chí có thể ssh vào nó (thủ thuật OpenVPN)


0

Cảm ơn @notz, câu trả lời của đang làm việc cho tôi trên Lolillop. Để giữ cho mã này hoạt động với SDK Android cũ, bạn có thể thực hiện mã này:

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

Cách bật Loa ngoài sau khi tự động trả lời cuộc gọi.

Tôi đã giải quyết vấn đề của mình ở trên với setSpeakerphoneOn. Tôi nghĩ rằng nó đáng để đăng ở đây, vì trường hợp sử dụng để tự động trả lời cuộc gọi điện thoại thường cũng yêu cầu loa ngoài hữu ích. Một lần nữa cảm ơn tất cả mọi người trên chủ đề này, thật là một công việc tuyệt vời.

Điều này phù hợp với tôi trên Android 5.1.1 trên Nexus 4 mà không cần ROOT. ;)

Quyền yêu cầu:

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

Mã 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
Hấp dẫn. Tôi thực sự đang cố gắng trả lời cuộc gọi và bật loa cùng nhau nên cách tiếp cận này dường như giải quyết được cả hai :). Tuy nhiên, tôi có câu hỏi tương tự như một số nhận xét trong các câu trả lời khác: mã này đi đâu?
fangmobile 29/06/16

-1

Chạy lệnh sau với quyền root:

input keyevent 5

Thêm chi tiết về mô phỏng keyevents tại đây .

Bạn có thể sử dụng lớp cơ sở này mà tôi đã tạo để chạy các lệnh dưới dạng root từ ứng dụng của bạn.


1
Trong khi thử nghiệm với hồ sơ người dùng thông thường, điều này đã hiển thị giao diện người dùng trong cuộc gọi cho tôi, yêu cầu tôi vuốt sang trái / phải để từ chối / trả lời hoặc sử dụng hành động / phản hồi nhanh. Nếu OP đang tạo màn hình cuộc gọi đến tùy chỉnh , thì điều này không thực sự giúp ích được gì trừ khi nó hoạt động khác dưới gốc, điều mà tôi nghi ngờ là nếu nó không hoạt động tốt đối với người dùng thông thường, cuộc gọi có thể chỉ thất bại và không kích hoạt một hành động khác.
Valter Jansons

-2

kiểm tra điều này: trước tiên hãy thêm các quyền sau đó sử dụng killCall () để gác máy, sử dụng answerCall () để trả lời cuộc gọi

<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 nếu bạn quan tâm đến cách KẾT THÚC cuộc gọi đang diễn ra trên Android O, Valter's sẽ Method 1: TelephonyManager.answerRingingCall()hoạt động nếu bạn thay đổi phương thức mà bạn gọi endCall.

Nó chỉ yêu cầu sự android.permission.CALL_PHONEcho phép.

Đây là mã:

// 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);
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.