【问题标题】:How can incoming calls be answered programmatically in Android 5.0 (Lollipop)?如何在 Android 5.0 (Lollipop) 中以编程方式接听来电?
【发布时间】:2014-11-14 06:59:54
【问题描述】:

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

【问题讨论】:

  • 哦,伙计,为什么会碰到这个,只是滑动伙计!对我来说似乎更容易\m/
  • 我正在为安卓用户创建一个自定义来电屏幕。
  • 有人吗?我也对这个感兴趣!尝试了很多东西,但没有奏效:/
  • @nobalG 他以编程方式说
  • @maveroid,您是否想出了适用于 Android 5.0 的解决方法?

标签: android android-5.0-lollipop phone-call incoming-call


【解决方案1】:

使用 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,以了解我们缺少有关特定“管道语法”的详细信息,但通过试验,它似乎必须充当“与”,表示所有指定的标志必须满足才能获得许可。在这种假设下工作,这意味着您必须拥有您的应用程序:

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

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

  2. 使用与框架/基础(即系统,即 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 list 看到。

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

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

这会导致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 answerRiley 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 是禁止使用的,或者根本没有文档。您应该谨慎行事。

【讨论】:

  • 关于耳机键码意图,您是否查看了 Google 源代码 here 是否出于任何原因停止对其采取行动?有趣的是,这些意图仍然可以很容易地拒绝呼叫(只是模拟长按),但没有任何响应。我还没有找到明确的权限检查或其他潜在的障碍,我希望第二组眼睛可能会发现一些东西。
  • 有点忙,因此延迟 - 将尝试花一些时间解决这个问题。快速浏览后,似乎 CallsManager 构造了 HeadsetMediaButton。 The session callback 应该注意在来自 MediaSessionManager 的回调时调用 handleHeadsetHook(KeyEvent)。所有代码似乎都匹配......但我想知道,有人可以删除 KeyEvent.ACTION_DOWN 意图进行测试吗? (也就是说,只触发一次 KeyEvent.ACTION_UP。)
  • 其实HeadsetMediaButton是和MediaSession一起工作的,并不直接和MediaSessionManager交互...
  • 对于那些设法找到原始方法似乎不起作用的情况(例如棒棒糖有问题)的人,我有好消息和坏消息:我已经设法让 ACTION_UP 工作 100 %,在我的代码中,带有 FULL_WAKE_LOCK。它不适用于 PARTIAL_WAKE_LOCK。绝对没有任何关于为什么会这样的文档。当我更广泛地测试我的实验代码时,我将在未来的答案中详细说明这一点。当然,坏消息是 FULL_WAKE_LOCK 已被弃用,所以这个修复只有在 Google 将其保留在 API 中时才会持续。
  • 在许多情况下不会调用原始答案中的捕获。我发现最好先调用 exec,然后再调用按钮。
【解决方案2】:

完整的解决方案基于@Valter Strods 代码。

要使其正常工作,您必须在执行代码的锁定屏幕上显示一个(不可见的)活动。

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>

呼叫接受活动

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

风格

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

终于召唤魔法了!

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

【讨论】:

  • 我想在哪里添加“终于调用魔法”下的代码。它是否适用于 Android 6.0
  • 我来这里是说 broadcastHeadsetConnected(boolean connected) 解决了三星 A3 2016 设备中的问题。没有它,一个非常相似的方法(使用单独的、透明的活动和线程进行调用等等)在大约 20 个经过测试的设备上完全有效,然后这个 A3 出现并迫使我重新检查这个问题以获得新的答案。与我的代码对比后,这就是显着的区别!
  • 如何拒接来电?您可以更新答案以显示这一点吗?
  • @leRobot 这个答案检查是否是HTC设备广播HeadsetConnected,如何检查它是否是三星A3 2016设备?顺便说一句,这确实是一个很好的答案,即使屏幕被锁定,我的应用也可以接听电话。
  • @eepty 您可以使用官方设备参考来获取构建数据。 (support.google.com/googleplay/answer/1727131?hl=en)。我将 Build.MODEL.startsWith("SM-A310") 用于 A3 2016。但是!我可以确认 A3 2016 不支持耳机连接广播!真正解决我的问题的是更改顺序,以便 Runtime.getRuntime().exec(... 首先为这些设备触发。它似乎每次都适用于该设备并且不会退回到异常。
【解决方案3】:

以下是对我有用的替代方法。它使用 MediaController API 直接将密钥事件发送到电信服务器。这要求应用具有BIND_NOTIFICATION_LISTENER_SERVICE 权限并且从用户那里获得了明确的通知访问权限:

@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 可能只是一个空类。

import android.service.notification.NotificationListenerService;

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

与清单中的相应部分:

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

由于事件的目标是明确的,这应该可以避免触发媒体播放器的任何副作用。

注意:电信服务器可能不会在振铃事件后立即激活。为了使其可靠地工作,应用程序实现MediaSessionManager.OnActiveSessionsChangedListener 以在发送事件之前监控电信服务器何时变为活动状态可能很有用。

更新:

Android O中,需要在ACTION_UP之前模拟ACTION_DOWN,否则上面没有效果。即需要以下内容:

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

但是,由于自 Android O 以来就可以使用接听电话的官方呼叫(请参阅最佳答案),因此可能不再需要此 hack,除非在 Android O 之前使用旧的编译 API 级别。

【讨论】:

  • 它对我不起作用。它返回权限错误。访问未授予应用程序的通知。我正在使用 Android L
  • 除了接受清单中的权限外,这还需要一个额外的步骤,即根据系统在设置菜单中的某处明确授予用户权限。
  • 报告这似乎适用于一个狭窄的案例:带有棉花糖的 Galaxy A3 2016。我将在一组 A3 设备中对此进行测试,这些设备由于 FATAL EXCEPTION: java.lang.SecurityException: Injecting to another application 需要 INJECT_EVENTS 权限而无法使用 input keyevent 方法。违规设备约占我用户群的 2%,我不会复制他们的异常,但会尝试这种方法,看看他们是否设法接听电话。幸运的是,我的应用程序已经在请求明确的通知。出于其他目的访问。
  • 经过广泛的测试,我很高兴地报告说,在 exec "input keyevent" 上失败的 A3 2016 设备可以使用 MediaController#dispatchMediaButtonEvent()) 方法。这显然仅在用户允许显式通知访问后才有效,因此您必须为此添加一个指向 Android 设置的屏幕,并且您基本上需要用户为此采取额外的操作,如答案中所述。在我的应用程序中,我们采取了额外的步骤来不断询问用户是否进入该屏幕但不添加通知。访问
  • 这适用于 Android Nougat。 @notz 的解决方案在其他方面效果很好,但在 Android 7 上抱怨“只有系统可以将媒体键事件发送到全局优先级会话”。
【解决方案4】:

要详细说明@Muzikant 的答案,并对其进行一些修改以在我的设备上更简洁,请尝试input keyevent 79,即KeyEvent.KEYCODE_HEADSETHOOK 的常量。 非常大致:

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

请原谅相当糟糕的编码约定,我不太精通 Runtime.exec() 调用。请注意,我的设备没有 root 权限,我也没有请求 root 权限。

这种方法的问题在于它只能在某些条件下工作(对我来说)。也就是说,如果我从用户在呼叫响铃时选择的菜单选项运行上述线程,呼叫就会很好地应答。如果我从监控来电状态的接收器运行它,它会被完全忽略。

所以在我的 Nexus 5 上,它非常适合用户驱动的应答,并且应该适合自定义呼叫屏幕的目的。它不适用于任何类型的自动呼叫控制类型的应用程序。

还需要注意的是所有可能的警告,包括这也可能会在一两次更新中停止工作。

【讨论】:

  • input keyevent 79 在 Sony Xperia 5.0 上运行良好。从活动或广播接收器调用时有效。
【解决方案5】:

通过 adb 命令 How to pick up a call by adb

请记住,Android 是在前端带有大量 JVM 的 Linux。你可以下载一个命令行应用程序并 root 手机,现在你有一台普通的 Linux 计算机和命令行来完成所有正常的事情。运行脚本,你甚至可以 ssh 到它(OpenVPN 技巧)

【讨论】:

    【解决方案6】:

    感谢@notz 的答案在棒棒糖上为我工作。为了使此代码与旧的 android SDK 一起工作,您可以执行以下代码:

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

    【讨论】:

      【解决方案7】:

      如何在自动接听电话后打开免提电话。

      我用 setSpeakerphoneOn 解决了我上面的问题。我认为它值得在这里发布,因为自动接听电话的用例通常也需要免提电话才有用。再次感谢这个帖子的每个人,多么棒的工作。

      这适用于我在没有 ROOT 的 Nexus 4 上的 Android 5.1.1 上。 ;)

      需要许可:

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

      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
      }
      

      【讨论】:

      • 有趣。我实际上是在尝试接听电话并一起打开扬声器,所以这种方法似乎可以解决这两个问题:)。不过,我和其他答案中的一些 cmets 有类似的问题:这段代码在哪里?
      【解决方案8】:

      以 root 身份运行以下命令:

      input keyevent 5
      

      更多关于模拟keyeventshere的细节。

      您可以使用 this base class I created 从您的应用以 root 身份运行命令。

      【讨论】:

      • 在使用常规用户配置文件进行测试时,这为我打开了通话界面,要求我向左/向右滑动以拒绝/回答或使用快速操作/响应。如果 OP 正在创建自定义来电屏幕,这实际上并没有任何帮助,除非它在 ​​root 下的行为不同,我怀疑它对于普通用户来说表现不佳,调用可能会简单地失败并且不会触发不同的操作。
      【解决方案9】:

      测试一下: 首先添加权限然后使用 killCall() 挂断使用 answerCall() 接听电话

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

      【讨论】:

        【解决方案10】:

        仅供参考,如果您对如何在 Android O 上结束正在进行的呼叫感兴趣,如果您将调用的方法更改为 endCall,Valter 的 Method 1: TelephonyManager.answerRingingCall() 就可以工作。

        它只需要android.permission.CALL_PHONE 权限。

        代码如下:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-20
          • 2023-03-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多