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

mav*_*oid 83 android phone-call incoming-call android-5.0-lollipop

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

Val*_*ons 137

使用Android 8.0 Oreo进行更新

虽然最初问的问题是Android L支持,但人们似乎仍然在回答这个问题和答案,所以值得描述Android 8.0 Oreo中引入的改进.向后兼容的方法仍在下面描述.

改变了什么?

Android 8.0 Oreo开始,PHONE权限组还包含ANSWER_PHONE_CALLS权限.正如权限的名称所示,持有它允许您的应用程序通过正确的API调用以编程方式接受传入的调用,而不会使用反射或模拟用户对系统进行任何黑客攻击.

我们如何利用这一变化?

如果您支持较旧的Android版本,则应在运行时检查系统版本,以便在保持对旧版Android版本的支持的同时封装此新API调用.您应该在运行时按照请求权限在运行时获取该新权限,这是较新的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();
Run Code Online (Sandbox Code Playgroud)

方法1:TelephonyManager.answerRingingCall()

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

这是什么?

TelephonyManager.answerRingingCall()是一个隐藏的内部方法.它作为ITelephony.answerRingingCall()的桥梁,已经在互联网上进行了讨论,并且在一开始就很有希望.它在4.4.2_r1不可用,因为它仅在Android 4.4 KitKat的提交83da75d(4.4.3_r1中的第1537行)中引入,后来在Lollipop的提交f1e1e77中 "重新引入"(5.038_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);
}
Run Code Online (Sandbox Code Playgroud)

这太好了,不可能!

实际上,有一个小问题.此方法应该完全正常,但安全管理器希望调用者持有android.permission.MODIFY_PHONE_STATE.此权限仅限于部分记录的系统功能,因为不希望第三方接触它(正如您可以从文档中看到的那样).您可以尝试添加<uses-permission>for,但这样做没有用,因为此权限的保护级别是签名系统(请参阅5.0.0_r1上core/AndroidManifest的第1201行).

你可以阅读问题34785:更新android:protectLevel文档,这是在2012年创建的,看看我们是否缺少有关特定"管道语法"的详细信息,但是通过试验,它似乎必须起到'AND'的作用,意味着所有的必须满足指定的标志才能授予权限.在这个假设下工作,这意味着你必须有你的申请:

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

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

  2. 签名与框架/ base相同的签名,即系统,即ROM.

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

此外,这种行为似乎是相关发行34792:安卓果冻豆/ 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_r44.4.4_r1),但在本地测试期间,代码5用于接听来电.由于Lollipo是一个大规模的更新,所以在这里改变是可以理解的内部.

这导致了一个命令service call phone 5.

我们如何以编程方式使用它?

Java的

以下代码是一个粗略的实现,用作概念证明.如果你真的想继续使用这种方法,你可能想要查看无问题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();
}
Run Code Online (Sandbox Code Playgroud)

表现

<!-- Inform the user we want them root accesses. -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
Run Code Online (Sandbox Code Playgroud)

这真的需要root访问吗?

可悲的是,它似乎如此.您可以尝试使用Runtime.exec,但我无法获得该路线的运气.

这有多稳定?

我很高兴你问.由于没有记录,这可以突破各种版本,如上面的看似代码差异所示.服务名称应该可以在各种构建中保持联系,但是对于我们所知道的,代码值可以在相同版本的多个构建中进行更改(例如,通过OEM的外观进行内部修改),从而破坏所使用的方法.因此值得一提的是,测试是在Nexus 4(mako/occam)上进行的.我个人建议你不要使用这种方法,但由于我无法找到更稳定的方法,我相信这是最好的方法.


原始方法:耳机键码意图

适合你必须安顿下来的时候.

以下部分受到Riley C的回答的强烈影响.

在原始问题中发布的模拟耳机意图方法似乎正如人们所期望的那样进行广播,但它似乎没有实现接听电话的目标.虽然似乎存在应该处理这些意图的代码,但它们根本就没有被关注,这意味着必须采取某种新的对策来对付这种方法.该日志也没有显示出任何有趣的内容,我个人认为通过Android源代码挖掘这一点是值得的,因为谷歌可能会引入一些轻微破坏所用方法的轻微变化.

我们现在能做些什么吗?

可以使用输入可执行文件一致地重现该行为.它接受一个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();
Run Code Online (Sandbox Code Playgroud)

TL;博士

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

Android 8.0 Oreo之前没有公共API.内部API是禁止的或只是没有文档.你应该谨慎行事.


not*_*otz 34

完全可行的解决方案基于@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>
Run Code Online (Sandbox Code Playgroud)

致电接受活动

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();
         }
     }
}
Run Code Online (Sandbox Code Playgroud)

样式

<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>
Run Code Online (Sandbox Code Playgroud)

最后召唤魔法!

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);
Run Code Online (Sandbox Code Playgroud)


hea*_*uck 13

以下是一种对我有用的替代方法.它使用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.");      
    }  
}
Run Code Online (Sandbox Code Playgroud)

NotificationReceiverService.class 在上面的代码中可能只是一个空类.

import android.service.notification.NotificationListenerService;

public class NotificationReceiverService extends NotificationListenerService{
     public NotificationReceiverService() {
     }
}
Run Code Online (Sandbox Code Playgroud)

使用清单中的相应部分:

    <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>
Run Code Online (Sandbox Code Playgroud)

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

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

更新:

Android的Ø,一个需要模拟ACTION_DOWN之前ACTION_UP,否则上面没有任何影响.即需要以下内容:

m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
Run Code Online (Sandbox Code Playgroud)

但是,自从Android O(请参阅最佳答案)之后可以获得正式的接听电话时,可能不再需要这种黑客攻击,除非在Android O之前遇到旧的编译API级别.

  • 除了接受清单中的权限之外,这还需要在设置菜单中的某个位置(取决于系统)明确授予用户权限的附加步骤. (2认同)

Ril*_*y C 9

要详细说明@Muzikant的答案,并稍微修改一下以便在我的设备上运行一点清洁,请尝试KeyEvent.KEYCODE_HEADSETHOOKinput keyevent 79的常量.非常粗略:

    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();
Run Code Online (Sandbox Code Playgroud)

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

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

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

同样值得注意的是所有可能的警告,包括这也可能会在一两个更新中停止工作.