使用 Android 8.0 Oreo 更新
尽管这个问题最初是针对 Android L 支持而提出的,但人们似乎仍然在回答这个问题和答案,因此值得描述 Android 8.0 Oreo 中引入的改进。向后兼容的方法仍然在下面描述。
发生了什么变化?
以Android 8.0 Oreo 开头,PHONE permission group 还包含ANSWER_PHONE_CALLS permission。正如权限的名称所暗示的那样,持有它可以让您的应用通过适当的 API 调用以编程方式接受传入调用,而无需使用反射或模拟用户对系统进行任何黑客攻击。
我们如何利用这一变化?
如果您支持较旧的 Android 版本,您应该check system version at runtime,以便您可以封装这个新的 API 调用,同时保持对那些较旧的 Android 版本的支持。您应该按照requesting permissions at run time 在运行时获取新权限,这是较新 Android 版本的标准。
获得许可后,您的应用只需调用TelecomManager's 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 (line 1537 on 4.4.3_r1) 的提交 83da75d 中引入,后来在 Lollipop (line 3138 on 5.0.0_r1) 的提交 f1e1e77 中“重新引入” ) 由于 Git 树的结构。这意味着除非你只支持带有 Lollipop 的设备,基于目前它的微小市场份额,这可能是一个糟糕的决定,如果沿着这条路线走,你仍然需要提供后备方法。
我们将如何使用它?
由于相关方法对 SDK 应用程序的使用是隐藏的,因此您需要使用 reflection 在运行时动态检查和使用该方法。如果对反射不熟悉,可以快速阅读What is reflection, and why is it useful?。如果您有兴趣,还可以通过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>,但这没有用,因为此权限的保护级别是signature|system (see line 1201 of core/AndroidManifest on 5.0.0_r1)。
您可以阅读 2012 年创建的 Issue 34785: Update android:protectionLevel documentation,以了解我们缺少有关特定“管道语法”的详细信息,但通过试验,它似乎必须充当“与”,表示所有指定的标志必须满足才能获得许可。在这种假设下工作,这意味着您必须拥有您的应用程序:
-
作为系统应用程序安装。
这应该没问题,可以通过要求用户在恢复中使用 ZIP 安装来完成,例如在未打包的自定义 ROM 上生根或安装 Google 应用程序时。
-
使用与框架/基础(即系统,即 ROM)相同的签名。
这就是问题出现的地方。为此,您需要掌握用于签署框架/基础的密钥。您不仅需要访问 Google 的 Nexus 工厂映像密钥,还必须访问所有其他 OEM 和 ROM 开发人员的密钥。这似乎不太合理,因此您可以通过制作自定义 ROM 并要求您的用户切换到它(这可能很难)或通过找到可以绕过权限保护级别的漏洞来让您的应用程序使用系统密钥进行签名(这也可能很难)。
此外,此行为似乎与 Issue 34792: Android Jelly Bean / 4.1: android.permission.READ_LOGS no longer works 相关,它使用相同的保护级别以及未记录的开发标志。
使用 TelephonyManager 听起来不错,但除非您获得适当的许可,否则将无法正常工作,这在实践中并不容易做到。
以其他方式使用 TelephonyManager 怎么样?
遗憾的是,您似乎需要持有android.permission.MODIFY_PHONE_STATE 才能使用这些很酷的工具,这反过来意味着您将很难使用这些方法。
方法二:服务调用SERVICE CODE
当您可以测试在设备上运行的构建是否可以使用指定的代码时。
在无法与 TelephonyManager 交互的情况下,也有可能通过 service 可执行文件与服务交互。
这是如何工作的?
这相当简单,但是关于这条路线的文档比其他路线还要少。我们确定可执行文件接受两个参数 - 服务名称和代码。
这会导致service call phone 5 命令。
我们如何以编程方式利用它?
Java
以下代码是作为概念验证的粗略实现。如果您真的想继续使用此方法,您可能想查看guidelines for problem-free su usage,并可能切换到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,但我无法通过该路线获得任何运气。
这有多稳定?
我很高兴你问。由于没有记录,这可能会跨越各种版本,如上面看似的代码差异所示。服务名称可能应该在各种构建中保持phone,但据我们所知,代码值可能会在同一版本的多个构建中发生变化(例如,OEM 皮肤的内部修改),进而破坏使用的方法。因此值得一提的是,测试是在 Nexus 4 (mako/occam) 上进行的。我个人会建议你不要使用这种方法,但由于我无法找到更稳定的方法,我相信这是最好的方法。
原始方法:耳机键码意图
当你不得不安顿下来的时候。
以下部分受到this answer 和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 是禁止使用的,或者根本没有文档。您应该谨慎行事。