如何在其他呼叫运行时以编程方式合并呼叫(电话会议)
Posted
技术标签:
【中文标题】如何在其他呼叫运行时以编程方式合并呼叫(电话会议)【英文标题】:How to merge Call programmatically while other call is running (Conference call) 【发布时间】:2014-01-23 22:56:02 【问题描述】:我的要求是这样的:假设我当时正在拨打一个号码,并且我想以编程方式拨打另一个号码。到目前为止,我所做的是:我可以在某些呼叫已经进行时呼叫特定号码。例如,假设我正在呼叫号码 123,并且在 1 分钟后(通过使用 Alarm Manger
我触发了一个事件来呼叫另一个号码 456,这就完成了!
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:456"));
startActivity(intent);
我正在使用这样的意图打电话,现在我可以通过一个按钮在手机上看到屏幕以合并通话:
在此图像中,您可以看到合并呼叫的按钮。现在,当用户单击合并时,它将合并所有 3 个呼叫。我想以编程方式完成,而不是使用用户界面。
【问题讨论】:
如果您找到任何解决方案、任何文章或任何其他关于此的内容,请分享。 您所在国家的运营商是否支持GSM电话会议? @user755 offcourse 是的 检查这里.. twilio.com/docs/client/android 嘿伙计...你是怎么打第二个电话的?如果我从 AlarmManager 尝试 startActivity,它会等到呼叫断开,然后它会继续...你能与我们分享你的代码吗? 【参考方案1】:您的问题似乎很有趣,因此我开始研究 Android Source。这是我发现的:
您发布的图片中的活动称为InCallUI
当您开始环顾四周时,您会发现InCallPresenter 在第 463 行有:
final boolean canMerge = activeCall.can(Capabilities.MERGE_CALLS);
然后在 472:
CallCommandClient.getInstance().merge();
当您检查 CallCommandClient 中的 merge() 方法时,您会发现它使用 ICallCommandService 接口,我认为这就是您要寻找的:)
该 CallCommandClient 的初始化位于第 193 行附近的 CallHandlerService。
希望这会有所帮助并祝你好运。
附言。我列出的 API 大多是 Android 内部的东西。您可能必须使用反射来调用它,或者它可能根本不可能 - 您的应用可能无法访问它,因为它没有被标记为系统应用。
【讨论】:
我按照您的代码执行此操作,但我收到 ClassNotFoundcom.android.incallui.CallCommandClient
。
@MeTTeO .... 我制作了一个自定义的 android.jar,我可以在其中访问所有内部和隐藏的 API 方法,但无法访问 InCallUI
, CallCommandClient
。我也无法使用 REFLECTION 访问它们。请说明您上面建议的方式。
你不能直接调用 CallCommandClient,因为这个类在 InCallUI 应用程序中,它是完全独立的 apk 包——不是 Android SDK 的一部分,而是一个独立的应用程序。相反,您应该看看 CallHandlerService 如何访问 ICallCommandService。
@MeTTeO,我已经完成了我可以的概念,但作为您的方法,无法在第三方应用程序中访问这些方法。所以我认为上述解释没有任何意义。那么,您能告诉我如何进行此操作吗?
@MeTTeO 我知道你建议的 API 也无法通过它访问,同样的标记说 - “已经制作了一个自定义 android.jar,我可以在其中访问所有内部和隐藏API 的方法,但无法访问 InCallUI , CallCommandClient。我也无法使用 REFLECTION 访问它们。请说明您上面建议的方式“【参考方案2】:
Android API 不支持调用合并功能,您可以查看此线程。 https://groups.google.com/forum/?fromgroups#!searchin/android-developers/conference$20call/android-developers/6OXDEe0tCks/8cuKdW1J9b8J 但是你可以做的是使用aidl打开手机的呼叫板屏幕,用户可以添加另一个呼叫或合并呼叫。
【讨论】:
我可以打电话给第三方,问题是如何合并,当我们建立第三个电话而已经有两个人在说话时,手机屏幕上出现合并选项,我不想要用户点击合并按钮并建立电话会议,我想以编程方式合并它 我不太确定,但您可以通过 performClick() 方法务实地调用事件,但您必须在其上搜索 ids 这就是我能做的事情,正如我的问题中提到的那样 @Williams 感谢您的批评。我真的接受您的想法,但我只是说它在 Android API 中不可用,并非在所有事物中都可用..而且我很想知道哪个应用程序的名称做同样的事情..这是一个系统应用程序吗?【参考方案3】:您无法使用智能手机管理会议。您需要一个可以为您执行此操作的中间服务。您可以使用CCXML 对会议管理器进行编程。
Voxeo 为 CCXML 实现提供了一个很好的托管平台,您可以查看他们的documentation,了解如何设置会议。在“学习CCXML 1.0\CCXML 1.0 中的多方会议”中有示例。
您可以在 Voxeo 上免费开发和测试,并且只有在您将其投入生产后才会开始向您收费。另一个选择是 Twillio。
这是how you program a conference call on their platform.的链接
查看链接,您将获得有用的信息。 #礼貌-所以
【讨论】:
很酷,但需要检查它是否适用于安卓手机,我们会尽快回复您【参考方案4】:Afaik,SDK 中没有以编程方式进行合并调用的 API。
您必须在RIL (Radio Interface Layer)
上工作,以便 android 用于电话会议。
Android 的无线电接口层 (RIL) 在 Android 电话服务 (android.telephony) 和无线电硬件之间提供了一个抽象层。 RIL 与无线电无关,包括对基于全球移动通信系统 (GSM) 的无线电的支持。
请看这里:http://www.kandroid.org/online-pdk/guide/telephony.html
更新
How does Modem code talk to Android code
http://fabiensanglard.net/cellphoneModem/index2.php
http://www.e-consystems.com/blog/android/?p=498
因此,您必须在套接字中编写 AT 调制解调器命令,然后 rild
调用供应商库的回调,然后供应商库依次委托给无线电固件。
【讨论】:
【参考方案5】:经过大量搜索,我在合并呼叫方面取得了成功,在这里我想与您分享我的发现。 作为参考,我使用了这个link
-
在您的项目中使用 CallList.java
package com.example.confrencecalldemo;
import android.os.Handler;
import android.os.Message;
import android.os.Trace;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class CallList
private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;
private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
private static CallList sInstance = new CallList();
private final HashMap<String, CallHelper> mCallById = new HashMap<>();
private final HashMap<android.telecom.Call, CallHelper> mCallByTelecommCall = new HashMap<>();
private final HashMap<String, List<String>> mCallTextReponsesMap = Maps.newHashMap();
/**
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
* load factor before resizing, 1 means we only expect a single thread to
* access the map so make only a single shard
*/
private final Set<Listener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
private final HashMap<String, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
.newHashMap();
private final Set<CallHelper> mPendingDisconnectCalls = Collections.newSetFromMap(
new ConcurrentHashMap<CallHelper, Boolean>(8, 0.9f, 1));
/**
* Static singleton accessor method.
*/
public static CallList getInstance()
return sInstance;
/**
* USED ONLY FOR TESTING
* Testing-only constructor. Instance should only be acquired through getInstance().
*/
CallList()
public void onCallAdded(android.telecom.Call telecommCall)
Trace.beginSection("onCallAdded");
CallHelper call = new CallHelper(telecommCall);
// Log.d(this, "onCallAdded: callState=" + call.getState());
if (call.getState() == CallHelper.State.INCOMING ||
call.getState() == CallHelper.State.CALL_WAITING)
onIncoming(call, call.getCannedSmsResponses());
else
onUpdate(call);
Trace.endSection();
public void onCallRemoved(android.telecom.Call telecommCall)
if (mCallByTelecommCall.containsKey(telecommCall))
CallHelper call = mCallByTelecommCall.get(telecommCall);
if (updateCallInMap(call))
// Log.w(this, "Removing call not previously disconnected " + call.getId());
updateCallTextMap(call, null);
/**
* Called when a single call disconnects.
*/
public void onDisconnect(CallHelper call)
if (updateCallInMap(call))
// Log.i(this, "onDisconnect: " + call);
// notify those listening for changes on this specific change
notifyCallUpdateListeners(call);
// notify those listening for all disconnects
notifyListenersOfDisconnect(call);
/**
* Called when a single call has changed.
*/
public void onIncoming(CallHelper call, List<String> textMessages)
if (updateCallInMap(call))
// Log.i(this, "onIncoming - " + call);
updateCallTextMap(call, textMessages);
for (Listener listener : mListeners)
listener.onIncomingCall(call);
public void onUpgradeToVideo(CallHelper call)
// Log.d(this, "onUpgradeToVideo call=" + call);
for (Listener listener : mListeners)
listener.onUpgradeToVideo(call);
/**
* Called when a single call has changed.
*/
public void onUpdate(CallHelper call)
Trace.beginSection("onUpdate");
onUpdateCall(call);
notifyGenericListeners();
Trace.endSection();
/**
* Called when a single call has changed session modification state.
*
* @param call The call.
* @param sessionModificationState The new session modification state.
*/
public void onSessionModificationStateChange(CallHelper call, int sessionModificationState)
final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
if (listeners != null)
for (CallUpdateListener listener : listeners)
listener.onSessionModificationStateChange(sessionModificationState);
/**
* Called when the last forwarded number changes for a call. With IMS, the last forwarded
* number changes due to a supplemental service notification, so it is not pressent at the
* start of the call.
*
* @param call The call.
*/
public void onLastForwardedNumberChange(CallHelper call)
final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
if (listeners != null)
for (CallUpdateListener listener : listeners)
listener.onLastForwardedNumberChange();
/**
* Called when the child number changes for a call. The child number can be received after a
* call is initially set up, so we need to be able to inform listeners of the change.
*
* @param call The call.
*/
public void onChildNumberChange(CallHelper call)
final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
if (listeners != null)
for (CallUpdateListener listener : listeners)
listener.onChildNumberChange();
public void notifyCallUpdateListeners(CallHelper call)
final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
if (listeners != null)
for (CallUpdateListener listener : listeners)
listener.onCallChanged(call);
/**
* Add a call update listener for a call id.
*
* @param callId The call id to get updates for.
* @param listener The listener to add.
*/
public void addCallUpdateListener(String callId, CallUpdateListener listener)
List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
if (listeners == null)
listeners = new CopyOnWriteArrayList<CallUpdateListener>();
mCallUpdateListenerMap.put(callId, listeners);
listeners.add(listener);
/**
* Remove a call update listener for a call id.
*
* @param callId The call id to remove the listener for.
* @param listener The listener to remove.
*/
public void removeCallUpdateListener(String callId, CallUpdateListener listener)
List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
if (listeners != null)
listeners.remove(listener);
public void addListener(Listener listener)
Preconditions.checkNotNull(listener);
mListeners.add(listener);
// Let the listener know about the active calls immediately.
listener.onCallListChange(this);
public void removeListener(Listener listener)
if (listener != null)
mListeners.remove(listener);
/**
* TODO: Change so that this function is not needed. Instead of assuming there is an active
* call, the code should rely on the status of a specific CallHelper and allow the presenters to
* update the CallHelper object when the active call changes.
*/
public CallHelper getIncomingOrActive()
CallHelper retval = getIncomingCall();
if (retval == null)
retval = getActiveCall();
return retval;
public CallHelper getOutgoingOrActive()
CallHelper retval = getOutgoingCall();
if (retval == null)
retval = getActiveCall();
return retval;
/**
* A call that is waiting for @link PhoneAccount selection
*/
public CallHelper getWaitingForAccountCall()
return getFirstCallWithState(CallHelper.State.SELECT_PHONE_ACCOUNT);
public CallHelper getPendingOutgoingCall()
return getFirstCallWithState(CallHelper.State.CONNECTING);
public CallHelper getOutgoingCall()
CallHelper call = getFirstCallWithState(CallHelper.State.DIALING);
if (call == null)
call = getFirstCallWithState(CallHelper.State.REDIALING);
return call;
public CallHelper getActiveCall()
return getFirstCallWithState(CallHelper.State.ACTIVE);
public CallHelper getBackgroundCall()
return getFirstCallWithState(CallHelper.State.ONHOLD);
public CallHelper getDisconnectedCall()
return getFirstCallWithState(CallHelper.State.DISCONNECTED);
public CallHelper getDisconnectingCall()
return getFirstCallWithState(CallHelper.State.DISCONNECTING);
public CallHelper getSecondBackgroundCall()
return getCallWithState(CallHelper.State.ONHOLD, 1);
public CallHelper getActiveOrBackgroundCall()
CallHelper call = getActiveCall();
if (call == null)
call = getBackgroundCall();
return call;
public CallHelper getIncomingCall()
CallHelper call = getFirstCallWithState(CallHelper.State.INCOMING);
if (call == null)
call = getFirstCallWithState(CallHelper.State.CALL_WAITING);
return call;
public CallHelper getFirstCall()
CallHelper result = getIncomingCall();
if (result == null)
result = getPendingOutgoingCall();
if (result == null)
result = getOutgoingCall();
if (result == null)
result = getFirstCallWithState(CallHelper.State.ACTIVE);
if (result == null)
result = getDisconnectingCall();
if (result == null)
result = getDisconnectedCall();
return result;
public boolean hasLiveCall()
CallHelper call = getFirstCall();
if (call == null)
return false;
return call != getDisconnectingCall() && call != getDisconnectedCall();
public CallHelper getVideoUpgradeRequestCall()
for(CallHelper call : mCallById.values())
if (call.getSessionModificationState() ==
CallHelper.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST)
return call;
return null;
public CallHelper getCallById(String callId)
return mCallById.get(callId);
public CallHelper getCallByTelecommCall(android.telecom.Call telecommCall)
return mCallByTelecommCall.get(telecommCall);
public List<String> getTextResponses(String callId)
return mCallTextReponsesMap.get(callId);
/**
* Returns first call found in the call map with the specified state.
*/
public CallHelper getFirstCallWithState(int state)
return getCallWithState(state, 0);
/**
* Returns the [position]th call found in the call map with the specified state.
* TODO: Improve this logic to sort by call time.
*/
public CallHelper getCallWithState(int state, int positionToFind)
CallHelper retval = null;
int position = 0;
for (CallHelper call : mCallById.values())
if (call.getState() == state)
if (position >= positionToFind)
retval = call;
break;
else
position++;
return retval;
/**
* This is called when the service disconnects, either expectedly or unexpectedly.
* For the expected case, it's because we have no calls left. For the unexpected case,
* it is likely a crash of phone and we need to clean up our calls manually. Without phone,
* there can be no active calls, so this is relatively safe thing to do.
*/
public void clearOnDisconnect()
for (CallHelper call : mCallById.values())
final int state = call.getState();
if (state != CallHelper.State.IDLE &&
state != CallHelper.State.INVALID &&
state != CallHelper.State.DISCONNECTED)
call.setState(CallHelper.State.DISCONNECTED);
call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
updateCallInMap(call);
notifyGenericListeners();
/**
* Called when the user has dismissed an error dialog. This indicates acknowledgement of
* the disconnect cause, and that any pending disconnects should immediately occur.
*/
public void onErrorDialogDismissed()
final Iterator<CallHelper> iterator = mPendingDisconnectCalls.iterator();
while (iterator.hasNext())
CallHelper call = iterator.next();
iterator.remove();
finishDisconnectedCall(call);
/**
* Processes an update for a single call.
*
* @param call The call to update.
*/
private void onUpdateCall(CallHelper call)
// Log.d(this, "\t" + call);
if (updateCallInMap(call))
// Log.i(this, "onUpdate - " + call);
updateCallTextMap(call, call.getCannedSmsResponses());
notifyCallUpdateListeners(call);
/**
* Sends a generic notification to all listeners that something has changed.
* It is up to the listeners to call back to determine what changed.
*/
private void notifyGenericListeners()
for (Listener listener : mListeners)
listener.onCallListChange(this);
private void notifyListenersOfDisconnect(CallHelper call)
for (Listener listener : mListeners)
listener.onDisconnect(call);
/**
* Updates the call entry in the local map.
* @return false if no call previously existed and no call was added, otherwise true.
*/
private boolean updateCallInMap(CallHelper call)
Preconditions.checkNotNull(call);
boolean updated = false;
if (call.getState() == CallHelper.State.DISCONNECTED)
// update existing (but do not add!!) disconnected calls
if (mCallById.containsKey(call.getId()))
// For disconnected calls, we want to keep them alive for a few seconds so that the
// UI has a chance to display anything it needs when a call is disconnected.
// Set up a timer to destroy the call after X seconds.
final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
mPendingDisconnectCalls.add(call);
mCallById.put(call.getId(), call);
mCallByTelecommCall.put(call.getTelecommCall(), call);
updated = true;
else if (!isCallDead(call))
mCallById.put(call.getId(), call);
mCallByTelecommCall.put(call.getTelecommCall(), call);
updated = true;
else if (mCallById.containsKey(call.getId()))
mCallById.remove(call.getId());
mCallByTelecommCall.remove(call.getTelecommCall());
updated = true;
return updated;
private int getDelayForDisconnect(CallHelper call)
Preconditions.checkState(call.getState() == CallHelper.State.DISCONNECTED);
final int cause = call.getDisconnectCause().getCode();
final int delay;
switch (cause)
case DisconnectCause.LOCAL:
delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
break;
case DisconnectCause.REMOTE:
case DisconnectCause.ERROR:
delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
break;
case DisconnectCause.REJECTED:
case DisconnectCause.MISSED:
case DisconnectCause.CANCELED:
// no delay for missed/rejected incoming calls and canceled outgoing calls.
delay = 0;
break;
default:
delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
break;
return delay;
private void updateCallTextMap(CallHelper call, List<String> textResponses)
Preconditions.checkNotNull(call);
if (!isCallDead(call))
if (textResponses != null)
mCallTextReponsesMap.put(call.getId(), textResponses);
else if (mCallById.containsKey(call.getId()))
mCallTextReponsesMap.remove(call.getId());
private boolean isCallDead(CallHelper call)
final int state = call.getState();
return CallHelper.State.IDLE == state || CallHelper.State.INVALID == state;
/**
* Sets up a call for deletion and notifies listeners of change.
*/
private void finishDisconnectedCall(CallHelper call)
if (mPendingDisconnectCalls.contains(call))
mPendingDisconnectCalls.remove(call);
call.setState(CallHelper.State.IDLE);
updateCallInMap(call);
notifyGenericListeners();
/**
* Notifies all video calls of a change in device orientation.
*
* @param rotation The new rotation angle (in degrees).
*/
public void notifyCallsOfDeviceRotation(int rotation)
for (CallHelper call : mCallById.values())
// First, ensure a VideoCall is set on the call so that the change can be sent to the
// provider (a VideoCall can be present for a call that does not currently have video,
// but can be upgraded to video).
// Second, ensure that the call videoState has video enabled (there is no need to set
// device orientation on a voice call which has not yet been upgraded to video).
if (call.getVideoCall() != null && CallUtils.isVideoCall(call))
call.getVideoCall().setDeviceOrientation(rotation);
/**
* Handles the timeout for destroying disconnected calls.
*/
private Handler mHandler = new Handler()
@Override
public void handleMessage(Message msg)
switch (msg.what)
case EVENT_DISCONNECTED_TIMEOUT:
// Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
finishDisconnectedCall((CallHelper) msg.obj);
break;
default:
// Log.wtf(this, "Message not expected: " + msg.what);
break;
;
/**
* Listener interface for any class that wants to be notified of changes
* to the call list.
*/
public interface Listener
/**
* Called when a new incoming call comes in.
* This is the only method that gets called for incoming calls. Listeners
* that want to perform an action on incoming call should respond in this method
* because @link #onCallListChange does not automatically get called for
* incoming calls.
*/
public void onIncomingCall(CallHelper call);
/**
* Called when a new modify call request comes in
* This is the only method that gets called for modify requests.
*/
public void onUpgradeToVideo(CallHelper call);
/**
* Called anytime there are changes to the call list. The change can be switching call
* states, updating information, etc. This method will NOT be called for new incoming
* calls and for calls that switch to disconnected state. Listeners must add actions
* to those method implementations if they want to deal with those actions.
*/
public void onCallListChange(CallList callList);
/**
* Called when a call switches to the disconnected state. This is the only method
* that will get called upon disconnection.
*/
public void onDisconnect(CallHelper call);
public interface CallUpdateListener
// TODO: refactor and limit arg to be call state. Caller info is not needed.
public void onCallChanged(CallHelper call);
/**
* Notifies of a change to the session modification state for a call.
*
* @param sessionModificationState The new session modification state.
*/
public void onSessionModificationStateChange(int sessionModificationState);
/**
* Notifies of a change to the last forwarded number for a call.
*/
public void onLastForwardedNumberChange();
/**
* Notifies of a change to the child number for a call.
*/
public void onChildNumberChange();
2.从InCallService类调用CallList.java的方法。
@Override
public void onCallAdded(Call call)
super.onCallAdded(call);
Log.d("MyConnectionService","onCallAdded");
CallList.getInstance().onCallAdded(call);
@Override
public void onCallRemoved(Call call)
super.onCallRemoved(call);
Log.d("MyConnectionService","onCallRemoved");
CallList.getInstance().onCallRemoved(call);
-
最终调用函数来合并调用
public void mergeCall() final CallList calls = CallList.getInstance(); CallHelper activeCall = calls.getActiveCall(); if (activeCall != null) final boolean canMerge = activeCall.can( android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); final boolean canSwap = activeCall.can( android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); // (2) Attempt actions on conference calls if (canMerge) TelecomAdapter.getInstance().merge(activeCall.getId()); else if (canSwap) TelecomAdapter.getInstance().swap(activeCall.getId());
我正在编辑我的答案,我忘了放置 CallHelper.java 类。 请访问以下链接获取 CallHelper.java 文件。
https://gist.github.com/amitsemwal1/4e9ca712adc8daaf070a0cc0e0d58c26
【讨论】:
CallHelper 类。我可以抓住它的地方。如果可能,请为我提供此调用合并实现的完整解决方案。 什么是 CallHelper?请指导。 @SanidhyaKumar 我已经更新了我的答案,请访问 CallHelper 课程的给定链接。【参考方案6】:android 中没有电话会议的 api,你可能想玩一下根系统并完成你的工作。
android 官方没有为电话会议提供任何 api。您可以在此处了解更多有关 root 访问权限的内容
http://www.kandroid.org/online-pdk/guide/telephony.html
【讨论】:
以上是关于如何在其他呼叫运行时以编程方式合并呼叫(电话会议)的主要内容,如果未能解决你的问题,请参考以下文章
如何以编程方式进行一响呼叫以及如何判断我呼叫的线路是不是已关闭?
使用没有alertView的UIWebView在iOS中以编程方式进行电话呼叫