如何在 Android 5.0 (Lollipop) 中以编程方式接听来电?

如何在 Android 5.0 (Lollipop) 中以编程方式接听来电?

问题描述:

当我尝试为来电创建自定义屏幕时,我尝试以编程方式接听来电.我正在使用以下代码,但它在 Android 5.0 中不起作用.

As I am trying to create a custom screen for incoming calls I am trying to programatically answer an incoming call. I am using the following code but it is not working in 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");

Update with Android 8.0 Oreo

尽管这个问题最初是为了 Android L 支持而提出的,但人们似乎仍然在回答这个问题,因此值得描述 Android 8.0 Oreo 中引入的改进.向后兼容的方法仍然在下面描述.

Update with Android 8.0 Oreo

Even though the question was originally asked for Android L support, people still seem to be hitting this question and answer, so it is worth describing the improvements introduced in Android 8.0 Oreo. The backward compatible methods are still described below.

Android 8.0 Oreo 开始,PHONE 权限组 还包含 ANSWER_PHONE_CALLS 权限.顾名思义,持有它允许您的应用程序通过适当的 API 调用以编程方式接受传入调用,而无需使用反射或模拟用户对系统进行任何黑客攻击.

Starting with Android 8.0 Oreo, the PHONE permission group also contains the ANSWER_PHONE_CALLS permission. As the name of permission suggests, holding it allows your app to programmatically accept incoming calls through a proper API call without any hacking around the system using reflection or simulating the user.

您应该在运行时检查系统版本 如果您支持旧的 Android 版本,以便您可以封装这个新的 API 调用,同时保持对那些旧的 Android 版本的支持.您应该按照在运行时请求权限在运行时获取新权限,这是较新的 Android 版本的标准.

You should check system version at runtime if you are supporting older Android versions so that you can encapsulate this new API call while maintaining support for those older Android versions. You should follow requesting permissions at run time to obtain that new permission during run-time, as is standard on the newer Android versions.

获得许可后,您的应用程序只需调用TelecomManager 的 acceptRingingCall 方法.一个基本的调用如下所示:

After having obtained the permission, your app just has to simply call the TelecomManager's acceptRingingCall method. A basic invocation looks as follows then:

TelecomManager tm = (TelecomManager) mContext
        .getSystemService(Context.TELECOM_SERVICE);

if (tm == null) {
    // whether you want to handle this is up to you really
    throw new NullPointerException("tm == null");
}

tm.acceptRingingCall();

方法 1:TelephonyManager.answerRingingCall()

当您可以无限制地控制设备时.

TelephonyManager.answerRingingCall() 是一个隐藏的内部方法.它作为 ITelephony.answerRingingCall() 的桥梁,已经在互联网上进行了讨论,一开始似乎很有希望.它在 4.4.2_r1 因为它仅在提交中引入 83da75d 适用于 Android 4.4 KitKat (4.4.3_r1 上的第 1537 行),随后在提交 f1e1e77 用于棒棒糖(5.0.0_r1 上的第 3138 行)由于 Git 树是结构化的.这意味着,除非你只支持 Lollipop 设备(鉴于目前它的市场份额很小,这可能是一个错误的决定),否则如果沿着这条路线走,你仍然需要提供后备方法.

There is TelephonyManager.answerRingingCall() which is a hidden, internal method. It works as a bridge for ITelephony.answerRingingCall() which has been discussed on the interwebs and seems promising at the start. It is not available on 4.4.2_r1 as it was introduced only in commit 83da75d for Android 4.4 KitKat (line 1537 on 4.4.3_r1) and later "reintroduced" in commit f1e1e77 for Lollipop (line 3138 on 5.0.0_r1) due to how the Git tree was structured. This means that unless you only support devices with Lollipop, which is probably a bad decision based on the tiny market share of it as of right now, you still need to provide fallback methods if going down this route.

由于该方法对 SDK 应用程序使用是隐藏的,您需要使用 reflection 在运行时动态检查和使用该方法.如果你对反射不熟悉,可以快速阅读什么是反射,为什么它有用?.如果您有兴趣,您还可以在 Trail: The Reflection API 中深入了解细节在这样做.

As the method in question is hidden from the SDK applications use, you need to use reflection to dynamically examine and use the method during runtime. If you are not familiar with reflection, you can quickly read What is reflection, and why is it useful?. You can also dig deeper into the specifics at Trail: The Reflection API if you are interested in doing so.

// set the logging tag constant; you probably want to change this
final String LOG_TAG = "TelephonyAnswer";

TelephonyManager tm = (TelephonyManager) mContext
        .getSystemService(Context.TELEPHONY_SERVICE);

try {
    if (tm == null) {
        // this will be easier for debugging later on
        throw new NullPointerException("tm == null");
    }

    // do reflection magic
    tm.getClass().getMethod("answerRingingCall").invoke(tm);
} catch (Exception e) {
    // we catch it all as the following things could happen:
    // NoSuchMethodException, if the answerRingingCall() is missing
    // SecurityException, if the security manager is not happy
    // IllegalAccessException, if the method is not accessible
    // IllegalArgumentException, if the method expected other arguments
    // InvocationTargetException, if the method threw itself
    // NullPointerException, if something was a null value along the way
    // ExceptionInInitializerError, if initialization failed
    // something more crazy, if anything else breaks

    // TODO decide how to handle this state
    // you probably want to set some failure state/go to fallback
    Log.e(LOG_TAG, "Unable to use the Telephony Manager directly.", e);
}

这好得令人难以置信!

实际上,有一个小问题.这个方法应该是全功能的,但是安全管理器希望调用者持有 android.权限.MODIFY_PHONE_STATE.此权限仅属于系统部分记录功能的领域,因为预计 3rd 方不会接触它(如您从其文档中所见).您可以尝试添加 但这不会有任何好处,因为此权限的保护级别是signature|system (参见 5.0.0_r1 上 core/AndroidManifest 的第 1201 行).

This is too good to be true!

Actually, there is one slight problem. This method should be fully functional, but the security manager wants callers to hold android.permission.MODIFY_PHONE_STATE. This permission is in the realm of only partially documented features of the system as 3rd parties are not expected to touch it (as you can see from the documentation for it). You can try adding a <uses-permission> for it but that will do no good because the protection level for this permission is signature|system (see line 1201 of core/AndroidManifest on 5.0.0_r1).

您可以阅读问题 34785:更新 android:protectionLevel 文档 它是在 2012 年创建的,目的是为了看到我们缺少有关特定管道语法"的详细信息,但是从周围的实验来看,它似乎必须用作AND",这意味着必须满足所有指定的标志才能获得许可被授予.在这种假设下工作,这意味着您必须拥有自己的应用程序:

You can read Issue 34785: Update android:protectionLevel documentation which was created back in 2012 to see that we are missing details about the specific "pipe syntax", but from experimenting around, it appears it must function as an 'AND' meaning all the specified flags have to be fulfilled for the permission to be granted. Working under that assumption, it would mean you must have your application:

  1. 作为系统应用程序安装.

这应该没问题,可以通过要求用户在恢复中使用 ZIP 进行安装来实现,例如在尚未打包的自定义 ROM 上生根或安装 Google 应用程序时.

This should be fine and could be accomplished by asking the users to install using a ZIP in recovery, such as when rooting or installing Google apps on custom ROMs that don't have them already packaged.

使用与框架/基础(又名系统,又名 ROM)相同的签名进行签名.

这就是问题出现的地方.为此,您需要掌握用于签署框架/基础的密钥.您不仅需要访问 Google 的 Nexus 工厂映像密钥,还必须访问所有其他 OEM 和 ROM 开发人员的密钥.这似乎不合理,因此您可以通过制作自定义 ROM 并要求您的用户切换到它(这可能很难)或通过找到可以绕过权限保护级别的漏洞来使用系统密钥对您的应用程序进行签名(这也可能很难).

This is where the problems pop up. To do this, you need to have your hands on the keys used for signing frameworks/base. You would not only have to get access to Google's keys for Nexus factory images, but you would also have to get access to all other OEMs' and ROM developers' keys. This does not seem plausible so you can have your application signed with the system keys by either making a custom ROM and asking your users to switch to it (which might be hard) or by finding an exploit with which the permission protection level can be bypassed (which might be hard as well).

此外,此行为似乎与问题 34792:Android Jelly 有关Bean/4.1:android.permission.READ_LOGS 不再有效,它使用相同的保护级别以及未记录的开发标志.

Additionally, this behavior appears to be related to Issue 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS no longer works which utilizes the same protection level along with an undocumented development flag as well.

使用 TelephonyManager 听起来不错,但除非您获得适当的许可,否则将无法工作,这在实践中并不容易.

遗憾的是,它似乎要求您持有 android.permission.MODIFY_PHONE_STATE 使用很酷的工具,这反过来意味着您将很难访问这些方法.

Sadly, it appears to require you to hold the android.permission.MODIFY_PHONE_STATE to use the cool tools which in turn means you are going to have a hard time getting access to those methods.

什么时候可以测试在设备上运行的构建是否可以使用指定的代码.

在无法与 TelephonyManager 交互的情况下,也有可能通过 service 可执行文件与服务交互.

Without being able to interact with the TelephonyManager, there is also the possibility of interacting with the service through the service executable.

这相当简单,但关于这条路线的文档比其他路线更少.我们确信可执行文件接受两个参数 - 服务名称和代码.

It is fairly simple, but there is even less documentation about this route than others. We know for sure the executable takes in two arguments - the service name and the code.

  • 我们要使用的服务名称电话.

这可以通过运行service list看到.

我们想要使用的代码看起来是6,但现在看起来是5.

The code we want to use appears to have been 6 but seems to now be 5.

看起来它是基于 IBinder.FIRST_CALL_TRANSACTION + 5 现在很多版本(来自 1.5_r44.4.4_r1) 但在本地测试期间代码 5 用于接听来电.由于 Lollipo 是一个全面的大规模更新,因此内部结构也发生了变化,这是可以理解的.

It looks like it has been based on IBinder.FIRST_CALL_TRANSACTION + 5 for many versions now (from 1.5_r4 to 4.4.4_r1) but during local testing the code 5 worked to answer an incoming call. As Lollipo is a massive update all around, it is understandable internals changed here as well.

这会产生 service call phone 5 命令.

以下代码是一个粗略的实现,用作概念证明.如果您真的想继续使用这种方法,您可能需要查看无问题 su 使用指南 并可能切换到更完善的 libsuperuser by Chainfire.

The following code is a rough implementation made to function as a proof of concept. If you actually want to go ahead and use this method, you probably want to check out guidelines for problem-free su usage and possibly switch to the more fully developed libsuperuser by Chainfire.

try {
    Process proc = Runtime.getRuntime().exec("su");
    DataOutputStream os = new DataOutputStream(proc.getOutputStream());

    os.writeBytes("service call phone 5
");
    os.flush();

    os.writeBytes("exit
");
    os.flush();

    if (proc.waitFor() == 255) {
        // TODO handle being declined root access
        // 255 is the standard code for being declined root for SU
    }
} catch (IOException e) {
    // TODO handle I/O going wrong
    // this probably means that the device isn't rooted
} catch (InterruptedException e) {
    // don't swallow interruptions
    Thread.currentThread().interrupt();
}

清单

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

这真的需要 root 访问权限吗?

很遗憾,似乎是这样.您可以尝试使用 Runtime.exec 在它上面,但我在这条路线上没有走运.

Does this really require root access?

Sadly, it seems so. You can try using Runtime.exec on it, but I was not able to get any luck with that route.

我很高兴你这么问.由于没有记录在案,这可能会跨越各种版本,如上面看似代码差异所示.服务名称可能应该在各种构建中保持电话,但就我们所知,代码值可能会在同一版本的多个构建中发生变化(例如,OEM 皮肤的内部修改)进而破坏使用的方法.因此值得一提的是,测试是在 Nexus 4 (mako/occam) 上进行的.我个人建议你不要使用这种方法,但由于我找不到更稳定的方法,我相信这是最好的方法.

I'm glad you asked. Due to not being documented, this can break across various versions, as illustrated by the seeming code difference above. The service name should probably stay phone across various builds, but for all we know, the code value can change across multiple builds of the same version (internal modifications by, say, the OEM's skin) in turn breaking the method used. It is therefore worth mentioning the testing took place on a Nexus 4 (mako/occam). I would personally advise you against using this method, but as I am not able to find a more stable method, I believe this is the best shot.

在您必须安顿下来的时候.

以下部分受到 此答案的强烈影响/users/1990699/riley-c">莱利 C.

The following section was strongly influenced by this answer by Riley C.

原始问题中发布的模拟耳机意图方法似乎正如人们所期望的那样广播,但它似乎并没有实现接听电话的目标.虽然似乎有代码来处理这些意图,但它们根本没有被关心,这意味着必须有某种新的对策来对抗这种方法.该日志也没有显示任何有趣的内容,而且我个人不认为为此挖掘 Android 源代码是值得的,因为 Google 可能会引入一些细微的更改,无论如何很容易破坏所使用的方法.

The simulated headset intent method as posted in the original question seems to be broadcast just as one would expect, but it doesn't appear to accomplish the goal of answering the call. While there appears to be code in place that should handle those intents, they simply aren't being cared about, which has to mean there must be some kind of new countermeasures in place against this method. The log doesn't show anything of interest either and I don't personally believe digging through the Android source for this will be worthwhile just due to the possibility of Google introducing a slight change that easily breaks the method used anyway.

使用输入可执行文件可以一致地重现该行为.它接受一个键码参数,我们只需传入 KeyEvent.KEYCODE_HEADSETHOOK.该方法甚至不需要 root 访问权限,使其适用于普通大众的常见用例,但该方法有一个小缺点——无法将耳机按钮按下事件指定为需要许可,这意味着它就像一个真实的按钮按下并在整个链中冒泡,这反过来意味着您必须谨慎考虑何时模拟按钮按下,因为它可能,例如,如果没有其他更高优先级的人准备好处理,则触发音乐播放器开始播放事件.

The behavior can be consistently reproduced using the input executable. It takes in a keycode argument, for which we simply pass in KeyEvent.KEYCODE_HEADSETHOOK. The method doesn't even require root access making it suitable for common use cases in the general public, but there is a small drawback in the method - the headset button press event cannot be specified to require a permission, meaning it works like a real button press and bubbles up through the whole chain, which in turn means you have to be cautious about when to simulate the button press as it could, for example, trigger the music player to start playback if nobody else of higher priority is ready to handle the event.

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;博士

Android 8.0 Oreo 及更高版本有一个很好的公共 API.


tl;dr

There is a nice public API for Android 8.0 Oreo and later.

在 Android 8.0 Oreo 之前没有公共 API.内部 API 是禁止使用的,或者根本没有文档.您应该谨慎行事.

There is no public API prior to Android 8.0 Oreo. The internal APIs are off-limits or simply without documentation. You should proceed with caution.