tangguo

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

android

当我尝试为来电创建自定义屏幕时,我试图以编程方式接听来电。我正在使用以下代码,但在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");

阅读 472

收藏
2020-12-01

共1个答案

小编典典

使用Android 8.0 Oreo更新
尽管最初要求提供Android L支持是一个问题,但似乎仍然有人在质疑这个问题和答案,因此值得描述Android 8.0 Oreo中引入的改进。向后兼容方法仍在下面描述。

发生了什么变化?
从Android 8.0 Oreo开始,PHONE权限组还包含ANSWER_PHONE_CALLS权限。就像许可名称所暗示的那样,持有该许可可以使您的应用通过适当的API调用以编程方式接受传入的调用,而无需使用反射或模拟用户对系统进行任何黑客攻击。

我们如何利用这一变化?
如果您支持较旧的Android版本,则应在运行时检查系统版本,以便可以封装此新的API调用,同时保持对这些较旧的Android版本的支持。您应该在运行时遵循请求权限,以在运行时获得新的权限,这是较新的Android版本的标准。

获得许可后,您的应用程序只需要简单地调用TelecomManager的acceptRingingCall方法。然后,基本调用如下所示:

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奇巧(上4.4.3_r1行1537)以及后来的“重新”在提交f1e1e77的棒棒堂(上5.0.0_r1线3138),由于该如何Git树结构化。这意味着,除非您仅支持使用Lollipop的设备(基于目前的棒极小市场份额,这可能是一个错误的决定),否则,您仍然需要提供备用方法。

我们将如何使用它?
由于有问题的方法对于SDK应用程序的使用是隐藏的,因此您需要使用反射在运行时动态检查和使用该方法。如果您不熟悉反射,则可以快速阅读什么是反射,它为什么有用?。如果您愿意的话,还可以在Trail:Reflection API中更深入地研究细节。

在代码中看起来如何?

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

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

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

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

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

这太好了,难以置信!
实际上,有一个小问题。此方法应具有完整的功能,但安全管理器希望调用方保留android.permission.MODIFY_PHONE_STATE。此权限仅存在部分记录在案的系统功能,因为不希望第三方接触到该功能(如从其文档中所见)。您可以尝试为其添加a ,但这将无济于事,因为此权限的保护级别为签名系统(请参阅5.0.0_r1上core / AndroidManifest的1201行)。

您可以阅读问题34785:更新于2012年创建的android:protectionLevel文档,以了解我们缺少有关特定“管道语法”的详细信息,但是通过试验,它似乎必须充当“ AND”,这意味着所有必须授予指定的标志才能授予许可。在该假设下工作,这意味着您必须拥有自己的申请:

作为系统应用程序安装。

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

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

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

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

使用TelephonyManager听起来不错,但是除非获得适当的许可,否则这是行不通的,在实践中这并不容易。

以其他方式使用TelephonyManager怎么办?
可悲的是,似乎要求您握住android.permission.MODIFY_PHONE_STATE才能使用出色的工具,这反过来意味着您将很难访问这些方法。

方法2:服务电话SERVICE CODE
有关何时可以测试设备上运行的版本是否可以使用指定代码的信息。

如果无法与TelephonyManager进行交互,则还可能通过service可执行文件与服务进行交互。

这是如何运作的?
这很简单,但是关于这条路线的文档甚至更少。我们肯定知道可执行文件带有两个参数-服务名称和代码。

我们要使用的服务名称是phone。

通过运行可以看到这一点service list。

我们要使用的代码似乎是6,但现在看来是5。

看起来它现在已经基于IBinder.FIRST_CALL_TRANSACTION + 5进行了许多版本(从1.5_r4到4.4.4_r1)的版本,但是在本地测试期间,代码5可以接听来电。由于Lollipo在各个方面都是一个巨大的更新,因此内部的变化也可以理解。

结果为的命令service call phone 5

我们如何以编程方式利用它?
爪哇
以下代码是一个粗略的实现,用作概念证明。如果你真的想继续使用这个方法,你可能想看看自由问题苏的使用指南,并可能切换到更充分的发展libsuperuser通过Chainfire。

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

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

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

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

表现

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

这真的需要root访问吗?
可悲的是,似乎是这样。您可以尝试在其上使用Runtime.exec,但是使用该路由我没有任何运气。

这有多稳定?
我很高兴你问。由于未记录在案,因此可能会跨越各种版本,如上面看似的代码差异所示。服务名称可能应该在各个版本之间保持联系,但就我们所知,代码值可能会在同一版本的多个版本(例如,OEM的皮肤内部修改)之间改变,从而破坏了所使用的方法。因此,值得一提的是该测试是在Nexus 4(mako / occam)上进行的。我个人会建议您不要使用此方法,但是由于找不到更稳定的方法,因此我认为这是最好的选择。

原始方法:耳机键码意图
有时需要定居。

下一节受Riley C的回答的影响很大。

原始问题中发布的模拟耳机意图方法似乎像人们期望的那样广播,但似乎并没有达到接听电话的目的。尽管似乎有适当的代码可以处理这些意图,但是根本就不在乎它们,这意味着必须有某种针对此方法的新对策。日志中也没有显示任何感兴趣的内容,我个人也不认为为此而在Android源代码中进行挖掘是值得的,这仅仅是因为Google可能会进行轻微的更改而轻易地改变了所使用的方法。

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

码?

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
Android 8.0 Oreo及更高版本有一个不错的公共API。

Android 8.0 Oreo之前没有公共API。内部API禁止使用,或者完全没有文档。您应谨慎行事。

2020-12-01