如何在Android 5.0(棒棒糖)中以编程方式回答传入呼叫?

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

使用Android 8.0 Oreo进行更新

尽管这个问题最初是针对Android L支持提出的,但人们似乎仍然会遇到这个问题和答案,所以值得描述Android 8.0 Oreo中引入的改进。 向后兼容的方法仍在下面描述。

什么改变了?

从Android 8.0 Oreo开始,PHONE权限组还包含ANSWER_PHONE_CALLS权限。 正如许可的名称所暗示的,通过持有它,您的应用程序可以通过适当的API调用以编程方式接受来电,而无需使用反射或模拟用户对系统进行任何黑客攻击。

我们如何利用这一变化?

如果您支持较旧的Android版本,那么您应该在运行时检查系统版本,以便您可以封装这个新的API调用,同时保持对那些较旧的Android版本的支持。 您应该在运行时遵循请求权限以在运行时获得新权限,这是新版A​​ndroid版本的标准。

获得许可后,您的应用程序只需调用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()的桥梁,已经在interwebs上讨论过,并且在开始时似乎很有前途。 它在4.4.2_r1上不可用,因为它仅在针对Android 4.4 KitKat的提交83da75d(4.4.3_r1上的线1537)中引入,并且稍后在针对Lollipop的提交f1e1e77(5.0.0_r1上的线3138)中“重新引入”,这是因为Git树是结构化的。 这意味着,除非您仅支持带有棒棒糖的设备,这可能是基于目前微小市场份额的糟糕决策,如果沿着这条路线走下去,您仍然需要提供备用方法。

我们将如何使用它?

由于所讨论的方法对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。 此权限仅属于系统的部分记录功能,因为第三方不会触及它(正如您可以从文档中看到的那样)。 您可以尝试为它添加一个<uses-permission> ,但这样做没有用,因为此权限的保护级别是signature | system(请参阅5.0.0_r1上的核心/ AndroidManifest的第1201行)。

您可以阅读Issue 34785:更新2012年创建的android:protectionLevel文档,以了解我们缺少有关特定“管道语法”的详细信息,但是从实验来看,它似乎必须作为“AND”运行,这意味着所有指定的标志必须满足授予的权限。 在这个假设下工作,这意味着你必须有你的申请:

  • 作为系统应用程序安装。

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

  • 与框架/基础也就是系统一样签名,也就是ROM。

    这是问题出现的地方。 为此,您需要掌握用于签署框架/基础的密钥。 您不仅需要访问Google的Nexus工厂映像密钥,还需要访问所有其他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可执行文件与服务交互。

    这个怎么用?

    这很简单,但有关这条路线的文件甚至比其他文件更少。 我们确信可执行文件需要两个参数 - 服务名称和代码。

  • 我们要使用的服务名称是电话。

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

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

    看起来它现在已经基于IBinder.FIRST_CALL_TRANSACTION + 5获得了许多版本(从1.5_r4到4.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 5n");
        os.flush();
    
        os.writeBytes("exitn");
        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是禁止的或者没有文档。 你应该谨慎行事。


    完全可行的解决方案基于@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);
    

    以下是为我工作的另一种方法。 它直接使用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开始正式接听电话以来(请参阅最佳答案),可能就不再需要这种攻击了,除非在Android O之前有人使用旧的编译API级别。

    链接地址: http://www.djcxy.com/p/47627.html

    上一篇: How can incoming calls be answered programmatically in Android 5.0 (Lollipop)?

    下一篇: Execute method from string