使用 Android 8.0 奥利奥更新
尽管这个问题最初是针对 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();
方法一:TelephonyManager.answerRingingCall()
当您可以无限控制设备时。
这是什么?
TelephonyManager.answerRingingCall() 是一种隐藏的内部方法。它作为 ITelephony.answerRingingCall() 的桥梁,已在互联网上进行了讨论,并且一开始似乎很有希望。它在4.4.2_r1上不可用,因为它仅在Android 4.4 KitKat的提交83da75d中引入(4.4.3_r1上的第 1537 行),后来在Lollipop的提交f1e1e77中“重新引入” (5.0.0_r1上的第 3138 行),因为Git 树是结构化的。这意味着,除非你只支持带有 Lollipop 的设备,基于目前它的微小市场份额,这可能是一个糟糕的决定,如果沿着这条路线走,你仍然需要提供后备方法。
我们将如何使用它?
由于相关方法对 SDK 应用程序的使用是隐藏的,因此您需要使用反射在运行时动态检查和使用该方法。如果不熟悉反射,可以快速阅读什么是反射,为什么有用?. 如果您有兴趣,还可以在Trail: The 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。此权限仅在系统的部分记录功能范围内,因为不希望第 3 方接触它(正如您从文档中看到的那样)。您可以尝试为其添加一个<uses-permission>
,但这不会有任何好处,因为此权限的保护级别是签名|系统(请参阅 5.0.0_r1 上 core/AndroidManifest 的第 1201 行)。
您可以阅读问题 34785:更新 android:protectionLevel 文档,该文档创建于 2012 年,以了解我们缺少有关特定“管道语法”的详细信息,但通过试验,它似乎必须充当“与”,这意味着所有必须满足指定的标志才能授予权限。在这种假设下工作,这意味着您必须拥有您的应用程序:
作为系统应用程序安装。
这应该没问题,可以通过要求用户在恢复中使用 ZIP 安装来完成,例如在尚未打包的自定义 ROM 上生根或安装 Google 应用程序时。
使用与框架/基础(即系统)相同的签名进行签名,即 ROM。
这就是问题出现的地方。为此,您需要掌握用于签署框架/基础的密钥。您不仅需要访问 Google 的 Nexus 工厂映像密钥,还必须访问所有其他 OEM 和 ROM 开发人员的密钥。这似乎不太合理,因此您可以通过制作自定义 ROM 并要求您的用户切换到它(这可能很难)或通过找到可以绕过权限保护级别的漏洞来让您的应用程序使用系统密钥进行签名(这也可能很难)。
此外,此行为似乎与问题 34792:Android Jelly Bean / 4.1:android.permission.READ_LOGS 不再有效,它使用相同的保护级别以及未记录的开发标志。
使用 TelephonyManager 听起来不错,但除非您获得适当的许可,否则将无法正常工作,这在实践中并不容易做到。
以其他方式使用 TelephonyManager 怎么样?
可悲的是,它似乎要求您持有android.permission.MODIFY_PHONE_STATE才能使用很酷的工具,这反过来意味着您将很难访问这些方法。
方法二:服务调用SERVICE CODE
当您可以测试在设备上运行的构建是否可以使用指定的代码时。
在无法与 TelephonyManager 交互的情况下,也有可能通过service
可执行文件与服务交互。
这是如何运作的?
这相当简单,但是关于这条路线的文档比其他路线还要少。我们确信可执行文件有两个参数——服务名称和代码。
这导致命令为service call phone 5
。
我们如何以编程方式利用它?
爪哇
以下代码是作为概念证明的粗略实现。如果你真的想继续使用这个方法,你可能想查看无问题的 su 使用指南,并可能切换到由 Chainfire 开发的更完整的libsuperuser。
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;博士
Android 8.0 Oreo 及更高版本有一个不错的公共 API。
在 Android 8.0 Oreo 之前没有公共 API。内部 API 是禁止使用的,或者根本没有文档。您应该谨慎行事。