/** * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr) * This file is part of CSipSimple. * * CSipSimple is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * If you own a pjsip commercial license you can also redistribute it * and/or modify it under the terms of the GNU Lesser General Public License * as an android library. * * CSipSimple is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with CSipSimple. If not, see <http://www.gnu.org/licenses/>. */ package com.csipsimple.ui.incall; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.media.AudioManager; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.RemoteException; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.util.SparseArray; import android.view.KeyEvent; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.RelativeLayout; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.csipsimple.R; import com.csipsimple.api.ISipService; import com.csipsimple.api.MediaState; import com.csipsimple.api.SipCallSession; import com.csipsimple.api.SipCallSession.StatusCode; import com.csipsimple.api.SipConfigManager; import com.csipsimple.api.SipManager; import com.csipsimple.api.SipProfile; import com.csipsimple.service.SipService; import com.csipsimple.ui.PickupSipUri; import com.csipsimple.ui.incall.CallProximityManager.ProximityDirector; import com.csipsimple.ui.incall.DtmfDialogFragment.OnDtmfListener; import com.csipsimple.ui.incall.locker.IOnLeftRightChoice; import com.csipsimple.ui.incall.locker.InCallAnswerControls; import com.csipsimple.ui.incall.locker.ScreenLocker; import com.csipsimple.utils.CallsUtils; import com.csipsimple.utils.DialingFeedback; import com.csipsimple.utils.Log; import com.csipsimple.utils.PreferencesProviderWrapper; import com.csipsimple.utils.Theme; import com.csipsimple.utils.keyguard.KeyguardWrapper; import org.webrtc.videoengine.ViERenderer; import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; public class InCallActivity extends SherlockFragmentActivity implements IOnCallActionTrigger, IOnLeftRightChoice, ProximityDirector, OnDtmfListener { private static final int QUIT_DELAY = 3000; private final static String THIS_FILE = "InCallActivity"; //private final static int DRAGGING_DELAY = 150; private Object callMutex = new Object(); private SipCallSession[] callsInfo = null; private MediaState lastMediaState; private ViewGroup mainFrame; private InCallControls inCallControls; // Screen wake lock for incoming call private WakeLock wakeLock; // Screen wake lock for video private WakeLock videoWakeLock; private InCallInfoGrid activeCallsGrid; private Timer quitTimer; // private LinearLayout detailedContainer, holdContainer; // True if running unit tests // private boolean inTest; private DialingFeedback dialFeedback; private PowerManager powerManager; private PreferencesProviderWrapper prefsWrapper; // Dnd views //private ImageView endCallTarget, holdTarget, answerTarget, xferTarget; //private Rect endCallTargetRect, holdTargetRect, answerTargetRect, xferTargetRect; private SurfaceView cameraPreview; private CallProximityManager proximityManager; private KeyguardWrapper keyguardManager; private boolean useAutoDetectSpeaker = false; private InCallAnswerControls inCallAnswerControls; private CallsAdapter activeCallsAdapter; private InCallInfoGrid heldCallsGrid; private CallsAdapter heldCallsAdapter; private final static int PICKUP_SIP_URI_XFER = 0; private final static int PICKUP_SIP_URI_NEW_CALL = 1; private static final String CALL_ID = "call_id"; @SuppressWarnings("deprecation") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //handler.setActivityInstance(this); Log.d(THIS_FILE, "Create in call"); setContentView(R.layout.in_call_main); SipCallSession initialSession = getIntent().getParcelableExtra(SipManager.EXTRA_CALL_INFO); synchronized (callMutex) { callsInfo = new SipCallSession[1]; callsInfo[0] = initialSession; } bindService(new Intent(this, SipService.class), connection, Context.BIND_AUTO_CREATE); prefsWrapper = new PreferencesProviderWrapper(this); // Log.d(THIS_FILE, "Creating call handler for " + // callInfo.getCallId()+" state "+callInfo.getRemoteContact()); powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, "com.csipsimple.onIncomingCall"); wakeLock.setReferenceCounted(false); takeKeyEvents(true); // Cache findViews mainFrame = (ViewGroup) findViewById(R.id.mainFrame); inCallControls = (InCallControls) findViewById(R.id.inCallControls); inCallAnswerControls = (InCallAnswerControls) findViewById(R.id.inCallAnswerControls); activeCallsGrid = (InCallInfoGrid) findViewById(R.id.activeCallsGrid); heldCallsGrid = (InCallInfoGrid) findViewById(R.id.heldCallsGrid); // Bind attachVideoPreview(); inCallControls.setOnTriggerListener(this); inCallAnswerControls.setOnTriggerListener(this); if(activeCallsAdapter == null) { activeCallsAdapter = new CallsAdapter(true); } activeCallsGrid.setAdapter(activeCallsAdapter); if(heldCallsAdapter == null) { heldCallsAdapter = new CallsAdapter(false); } heldCallsGrid.setAdapter(heldCallsAdapter); ScreenLocker lockOverlay = (ScreenLocker) findViewById(R.id.lockerOverlay); lockOverlay.setActivity(this); lockOverlay.setOnLeftRightListener(this); /* middleAddCall = (Button) findViewById(R.id.add_call_button); middleAddCall.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { onTrigger(ADD_CALL, null); } }); if (!prefsWrapper.getPreferenceBooleanValue(SipConfigManager.SUPPORT_MULTIPLE_CALLS)) { middleAddCall.setEnabled(false); middleAddCall.setText(R.string.not_configured_multiple_calls); } */ // Listen to media & sip events to update the UI registerReceiver(callStateReceiver, new IntentFilter(SipManager.ACTION_SIP_CALL_CHANGED)); registerReceiver(callStateReceiver, new IntentFilter(SipManager.ACTION_SIP_MEDIA_CHANGED)); registerReceiver(callStateReceiver, new IntentFilter(SipManager.ACTION_ZRTP_SHOW_SAS)); proximityManager = new CallProximityManager(this, this, lockOverlay); keyguardManager = KeyguardWrapper.getKeyguardManager(this); dialFeedback = new DialingFeedback(this, true); if (prefsWrapper.getPreferenceBooleanValue(SipConfigManager.PREVENT_SCREEN_ROTATION)) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } if (quitTimer == null) { quitTimer = new Timer("Quit-timer"); } useAutoDetectSpeaker = prefsWrapper.getPreferenceBooleanValue(SipConfigManager.AUTO_DETECT_SPEAKER); applyTheme(); proximityManager.startTracking(); inCallControls.setCallState(initialSession); inCallAnswerControls.setCallState(initialSession); } @Override protected void onStart() { Log.d(THIS_FILE, "Start in call"); super.onStart(); keyguardManager.unlock(); } @Override protected void onResume() { super.onResume(); /* endCallTargetRect = null; holdTargetRect = null; answerTargetRect = null; xferTargetRect = null; */ dialFeedback.resume(); runOnUiThread(new UpdateUIFromCallRunnable()); } @Override protected void onPause() { super.onPause(); dialFeedback.pause(); } @Override protected void onStop() { super.onStop(); keyguardManager.lock(); } @Override protected void onDestroy() { if(infoDialog != null) { infoDialog.dismiss(); } if (quitTimer != null) { quitTimer.cancel(); quitTimer.purge(); quitTimer = null; } /* if (draggingTimer != null) { draggingTimer.cancel(); draggingTimer.purge(); draggingTimer = null; } */ try { unbindService(connection); } catch (Exception e) { // Just ignore that } service = null; if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); } proximityManager.stopTracking(); proximityManager.release(0); try { unregisterReceiver(callStateReceiver); } catch (IllegalArgumentException e) { // That's the case if not registered (early quit) } if(activeCallsGrid != null) { activeCallsGrid.terminate(); } detachVideoPreview(); //handler.setActivityInstance(null); super.onDestroy(); } @SuppressWarnings("deprecation") private void attachVideoPreview() { // Video stuff if(prefsWrapper.getPreferenceBooleanValue(SipConfigManager.USE_VIDEO)) { if(cameraPreview == null) { Log.d(THIS_FILE, "Create Local Renderer"); cameraPreview = ViERenderer.CreateLocalRenderer(this); RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(256, 256); //lp.leftMargin = 2; //lp.topMargin= 4; lp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE); cameraPreview.setVisibility(View.GONE); mainFrame.addView(cameraPreview, lp); }else { Log.d(THIS_FILE, "NO NEED TO Create Local Renderer"); } if(videoWakeLock == null) { videoWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "com.csipsimple.videoCall"); videoWakeLock.setReferenceCounted(false); } } if(videoWakeLock != null && videoWakeLock.isHeld()) { videoWakeLock.release(); } } private void detachVideoPreview() { if(mainFrame != null && cameraPreview != null) { mainFrame.removeView(cameraPreview); } if(videoWakeLock != null && videoWakeLock.isHeld()) { videoWakeLock.release(); } if(cameraPreview != null) { cameraPreview = null; } } @Override protected void onNewIntent(Intent intent) { setIntent(intent); // TODO : update UI Log.d(THIS_FILE, "New intent is launched"); super.onNewIntent(intent); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); Log.d(THIS_FILE, "Configuration changed"); if(cameraPreview != null && cameraPreview.getVisibility() == View.VISIBLE) { cameraPreview.setVisibility(View.GONE); } runOnUiThread(new UpdateUIFromCallRunnable()); } private void applyTheme() { Theme t = Theme.getCurrentTheme(this); if (t != null) { // TODO ... } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case PICKUP_SIP_URI_XFER: if (resultCode == RESULT_OK && service != null) { String callee = data.getStringExtra(Intent.EXTRA_PHONE_NUMBER); int callId = data.getIntExtra(CALL_ID, -1); if(callId != -1) { try { service.xfer((int) callId, callee); } catch (RemoteException e) { // TODO : toaster } } } return; case PICKUP_SIP_URI_NEW_CALL: if (resultCode == RESULT_OK && service != null) { String callee = data.getStringExtra(Intent.EXTRA_PHONE_NUMBER); long accountId = data.getLongExtra(SipProfile.FIELD_ID, SipProfile.INVALID_ID); if (accountId != SipProfile.INVALID_ID) { try { service.makeCall(callee, (int) accountId); } catch (RemoteException e) { // TODO : toaster } } } return; default: break; } super.onActivityResult(requestCode, resultCode, data); } /** * Get the call that is active on the view * * @param excludeHold if true we do not return cals hold locally * @return */ private SipCallSession getActiveCallInfo() { SipCallSession currentCallInfo = null; if (callsInfo == null) { return null; } for (SipCallSession callInfo : callsInfo) { currentCallInfo = getPrioritaryCall(callInfo, currentCallInfo); } return currentCallInfo; } /** * Get the call with the higher priority comparing two calls * @param call1 First call object to compare * @param call2 Second call object to compare * @return The call object with highest priority */ private SipCallSession getPrioritaryCall(SipCallSession call1, SipCallSession call2) { // We prefer the not null if (call1 == null) { return call2; } else if (call2 == null) { return call1; } // We prefer the one not terminated if (call1.isAfterEnded()) { return call2; } else if (call2.isAfterEnded()) { return call1; } // We prefer the one not held if (call1.isLocalHeld()) { return call2; } else if (call2.isLocalHeld()) { return call1; } // We prefer the older call // to keep consistancy on what will be replied if new call arrives return (call1.getCallStart() > call2.getCallStart()) ? call2 : call1; } /** * Update the user interface from calls state. */ private class UpdateUIFromCallRunnable implements Runnable { @Override public void run() { // Current call is the call emphasis by the UI. SipCallSession mainCallInfo = null; int mainsCalls = 0; int heldsCalls = 0; synchronized (callMutex) { if (callsInfo != null) { for (SipCallSession callInfo : callsInfo) { Log.d(THIS_FILE, "We have a call " + callInfo.getCallId() + " / " + callInfo.getCallState() + "/" + callInfo.getMediaStatus()); if (!callInfo.isAfterEnded()) { if (callInfo.isLocalHeld()) { heldsCalls++; } else { mainsCalls++; } } mainCallInfo = getPrioritaryCall(callInfo, mainCallInfo); } } } // Update call control visibility - must be done before call cards // because badge avail size depends on that if ((mainsCalls + heldsCalls) >= 1) { // Update in call actions inCallControls.setCallState(mainCallInfo); inCallAnswerControls.setCallState(mainCallInfo); } else { inCallControls.setCallState(null); inCallAnswerControls.setCallState(null); } heldCallsGrid.setVisibility((heldsCalls > 0)? View.VISIBLE : View.GONE); activeCallsAdapter.notifyDataSetChanged(); heldCallsAdapter.notifyDataSetChanged(); //findViewById(R.id.inCallContainer).requestLayout(); if (mainCallInfo != null) { Log.d(THIS_FILE, "Active call is " + mainCallInfo.getCallId()); Log.d(THIS_FILE, "Update ui from call " + mainCallInfo.getCallId() + " state " + CallsUtils.getStringCallState(mainCallInfo, InCallActivity.this)); int state = mainCallInfo.getCallState(); //int backgroundResId = R.drawable.bg_in_call_gradient_unidentified; // We manage wake lock switch (state) { case SipCallSession.InvState.INCOMING: case SipCallSession.InvState.EARLY: case SipCallSession.InvState.CALLING: case SipCallSession.InvState.CONNECTING: Log.d(THIS_FILE, "Acquire wake up lock"); if (wakeLock != null && !wakeLock.isHeld()) { wakeLock.acquire(); } break; case SipCallSession.InvState.CONFIRMED: break; case SipCallSession.InvState.NULL: case SipCallSession.InvState.DISCONNECTED: Log.d(THIS_FILE, "Active call session is disconnected or null wait for quit..."); // This will release locks onDisplayVideo(false); delayedQuit(); return; } Log.d(THIS_FILE, "we leave the update ui function"); } proximityManager.updateProximitySensorMode(); if (heldsCalls + mainsCalls == 0) { delayedQuit(); } } } @Override public void onDisplayVideo(boolean show) { runOnUiThread(new UpdateVideoPreviewRunnable(show)); } /** * Update ui from media state. */ private class UpdateUIFromMediaRunnable implements Runnable { @Override public void run() { inCallControls.setMediaState(lastMediaState); proximityManager.updateProximitySensorMode(); } } private class UpdateVideoPreviewRunnable implements Runnable { private final boolean show; UpdateVideoPreviewRunnable(boolean show){ this.show = show; } @Override public void run() { // Update the camera preview visibility if(cameraPreview != null) { cameraPreview.setVisibility(show ? View.VISIBLE : View.GONE); if(show) { if(videoWakeLock != null) { videoWakeLock.acquire(); } SipService.setVideoWindow(SipCallSession.INVALID_CALL_ID, cameraPreview, true); }else { if(videoWakeLock != null && videoWakeLock.isHeld()) { videoWakeLock.release(); } SipService.setVideoWindow(SipCallSession.INVALID_CALL_ID, null, true); } }else { Log.w(THIS_FILE, "No camera preview available to be shown"); } } } /* private void setSubViewVisibilitySafely(int id, boolean visible) { View v = findViewById(id); if(v != null) { v.setVisibility(visible ? View.VISIBLE : View.GONE); } } private class UpdateDraggingRunnable implements Runnable { private DraggingInfo di; UpdateDraggingRunnable(DraggingInfo draggingInfo){ di = draggingInfo; } public void run() { inCallControls.setVisibility(di.isDragging ? View.GONE : View.VISIBLE); findViewById(R.id.dropZones).setVisibility(di.isDragging ? View.VISIBLE : View.GONE); setSubViewVisibilitySafely(R.id.dropHangup, di.isDragging); setSubViewVisibilitySafely(R.id.dropHold, (di.isDragging && di.call.isActive() && !di.call.isBeforeConfirmed())); setSubViewVisibilitySafely(R.id.dropAnswer, (di.call.isActive() && di.call.isBeforeConfirmed() && di.call.isIncoming() && di.isDragging)); setSubViewVisibilitySafely(R.id.dropXfer, (!di.call.isBeforeConfirmed() && !di.call.isAfterEnded() && di.isDragging)); } } */ private synchronized void delayedQuit() { if (wakeLock != null && wakeLock.isHeld()) { Log.d(THIS_FILE, "Releasing wake up lock"); wakeLock.release(); } proximityManager.release(0); activeCallsGrid.setVisibility(View.VISIBLE); inCallControls.setVisibility(View.GONE); Log.d(THIS_FILE, "Start quit timer"); if (quitTimer != null) { quitTimer.schedule(new QuitTimerTask(), QUIT_DELAY); } else { finish(); } } private class QuitTimerTask extends TimerTask { @Override public void run() { Log.d(THIS_FILE, "Run quit timer"); finish(); } }; private void showDialpad(int callId) { DtmfDialogFragment newFragment = DtmfDialogFragment.newInstance(callId); newFragment.show(getSupportFragmentManager(), "dialog"); } @Override public void OnDtmf(int callId, int keyCode, int dialTone) { proximityManager.restartTimer(); if (service != null) { if (callId != SipCallSession.INVALID_CALL_ID) { try { service.sendDtmf(callId, keyCode); dialFeedback.giveFeedback(dialTone); } catch (RemoteException e) { Log.e(THIS_FILE, "Was not able to send dtmf tone", e); } } } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Log.d(THIS_FILE, "Key down : " + keyCode); switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: // // Volume has been adjusted by the user. // Log.d(THIS_FILE, "onKeyDown: Volume button pressed"); int action = AudioManager.ADJUST_RAISE; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { action = AudioManager.ADJUST_LOWER; } // Detect if ringing SipCallSession currentCallInfo = getActiveCallInfo(); // If not any active call active if (currentCallInfo == null && serviceConnected) { break; } if (service != null) { try { service.adjustVolume(currentCallInfo, action, AudioManager.FLAG_SHOW_UI); } catch (RemoteException e) { Log.e(THIS_FILE, "Can't adjust volume", e); } } return true; case KeyEvent.KEYCODE_CALL: case KeyEvent.KEYCODE_ENDCALL: return inCallAnswerControls.onKeyDown(keyCode, event); case KeyEvent.KEYCODE_SEARCH: // Prevent search return true; default: // Nothing to do } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { Log.d(THIS_FILE, "Key up : " + keyCode); switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_CALL: case KeyEvent.KEYCODE_SEARCH: return true; case KeyEvent.KEYCODE_ENDCALL: return inCallAnswerControls.onKeyDown(keyCode, event); } return super.onKeyUp(keyCode, event); } private BroadcastReceiver callStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(SipManager.ACTION_SIP_CALL_CHANGED)) { if (service != null) { try { synchronized (callMutex) { callsInfo = service.getCalls(); runOnUiThread(new UpdateUIFromCallRunnable()); } } catch (RemoteException e) { Log.e(THIS_FILE, "Not able to retrieve calls"); } } } else if (action.equals(SipManager.ACTION_SIP_MEDIA_CHANGED)) { if (service != null) { MediaState mediaState; try { mediaState = service.getCurrentMediaState(); Log.d(THIS_FILE, "Media update ...." + mediaState.isSpeakerphoneOn); synchronized (callMutex) { if (!mediaState.equals(lastMediaState)) { lastMediaState = mediaState; runOnUiThread(new UpdateUIFromMediaRunnable()); } } } catch (RemoteException e) { Log.e(THIS_FILE, "Can't get the media state ", e); } } } else if (action.equals(SipManager.ACTION_ZRTP_SHOW_SAS)) { SipCallSession callSession = intent.getParcelableExtra(SipManager.EXTRA_CALL_INFO); String sas = intent.getStringExtra(Intent.EXTRA_SUBJECT); runOnUiThread(new ShowZRTPInfoRunnable(callSession, sas)); } } }; /** * Service binding */ private boolean serviceConnected = false; private ISipService service; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName arg0, IBinder arg1) { service = ISipService.Stub.asInterface(arg1); try { // Log.d(THIS_FILE, // "Service started get real call info "+callInfo.getCallId()); callsInfo = service.getCalls(); serviceConnected = true; runOnUiThread(new UpdateUIFromCallRunnable()); runOnUiThread(new UpdateUIFromMediaRunnable()); } catch (RemoteException e) { Log.e(THIS_FILE, "Can't get back the call", e); } } @Override public void onServiceDisconnected(ComponentName arg0) { serviceConnected = false; callsInfo = null; } }; private AlertDialog infoDialog; // private boolean showDetails = true; @Override public void onTrigger(int whichAction, final SipCallSession call) { // Sanity check for actions requiring valid call id if (whichAction == TAKE_CALL || whichAction == REJECT_CALL || whichAction == DONT_TAKE_CALL || whichAction == TERMINATE_CALL || whichAction == DETAILED_DISPLAY || whichAction == TOGGLE_HOLD || whichAction == START_RECORDING || whichAction == STOP_RECORDING || whichAction == DTMF_DISPLAY || whichAction == XFER_CALL || whichAction == TRANSFER_CALL || whichAction == START_VIDEO || whichAction == STOP_VIDEO ) { // We check that current call is valid for any actions if (call == null) { Log.e(THIS_FILE, "Try to do an action on a null call !!!"); return; } if (call.getCallId() == SipCallSession.INVALID_CALL_ID) { Log.e(THIS_FILE, "Try to do an action on an invalid call !!!"); return; } } // Reset proximity sensor timer proximityManager.restartTimer(); try { switch (whichAction) { case TAKE_CALL: { if (service != null) { Log.d(THIS_FILE, "Answer call " + call.getCallId()); boolean shouldHoldOthers = false; // Well actually we should be always before confirmed if (call.isBeforeConfirmed()) { shouldHoldOthers = true; } service.answer(call.getCallId(), SipCallSession.StatusCode.OK); // if it's a ringing call, we assume that user wants to // hold other calls if (shouldHoldOthers && callsInfo != null) { for (SipCallSession callInfo : callsInfo) { // For each active and running call if (SipCallSession.InvState.CONFIRMED == callInfo.getCallState() && !callInfo.isLocalHeld() && callInfo.getCallId() != call.getCallId()) { Log.d(THIS_FILE, "Hold call " + callInfo.getCallId()); service.hold(callInfo.getCallId()); } } } } break; } case DONT_TAKE_CALL: { if (service != null) { service.hangup(call.getCallId(), StatusCode.BUSY_HERE); } break; } case REJECT_CALL: case TERMINATE_CALL: { if (service != null) { service.hangup(call.getCallId(), 0); } break; } case MUTE_ON: case MUTE_OFF: { if (service != null) { service.setMicrophoneMute((whichAction == MUTE_ON) ? true : false); } break; } case SPEAKER_ON: case SPEAKER_OFF: { if (service != null) { Log.d(THIS_FILE, "Manually switch to speaker"); useAutoDetectSpeaker = false; service.setSpeakerphoneOn((whichAction == SPEAKER_ON) ? true : false); } break; } case BLUETOOTH_ON: case BLUETOOTH_OFF: { if (service != null) { service.setBluetoothOn((whichAction == BLUETOOTH_ON) ? true : false); } break; } case DTMF_DISPLAY: { showDialpad(call.getCallId()); break; } case DETAILED_DISPLAY: { if (service != null) { if(infoDialog != null) { infoDialog.dismiss(); } String infos = service.showCallInfosDialog(call.getCallId()); String natType = service.getLocalNatType(); SpannableStringBuilder buf = new SpannableStringBuilder(); Builder builder = new AlertDialog.Builder(this); buf.append(infos); if(!TextUtils.isEmpty(natType)) { buf.append("\r\nLocal NAT type detected : "); buf.append(natType); } TextAppearanceSpan textSmallSpan = new TextAppearanceSpan(this, android.R.style.TextAppearance_Small); buf.setSpan(textSmallSpan, 0, buf.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); infoDialog = builder.setIcon(android.R.drawable.ic_dialog_info) .setMessage(buf) .setNeutralButton(R.string.ok, null) .create(); infoDialog.show(); } break; } case TOGGLE_HOLD: { if (service != null) { // Log.d(THIS_FILE, // "Current state is : "+callInfo.getCallState().name()+" / "+callInfo.getMediaStatus().name()); if (call.getMediaStatus() == SipCallSession.MediaState.LOCAL_HOLD || call.getMediaStatus() == SipCallSession.MediaState.NONE) { service.reinvite(call.getCallId(), true); } else { service.hold(call.getCallId()); } } break; } case MEDIA_SETTINGS: { startActivity(new Intent(this, InCallMediaControl.class)); break; } case XFER_CALL: { Intent pickupIntent = new Intent(this, PickupSipUri.class); pickupIntent.putExtra(CALL_ID, call.getCallId()); startActivityForResult(pickupIntent, PICKUP_SIP_URI_XFER); break; } case TRANSFER_CALL: { final ArrayList<SipCallSession> remoteCalls = new ArrayList<SipCallSession>(); if(callsInfo != null) { for(SipCallSession remoteCall : callsInfo) { // Verify not current call if(remoteCall.getCallId() != call.getCallId() && remoteCall.isOngoing()) { remoteCalls.add(remoteCall); } } } if(remoteCalls.size() > 0) { Builder builder = new AlertDialog.Builder(this); CharSequence[] simpleAdapter = new String[remoteCalls.size()]; for(int i = 0; i < remoteCalls.size(); i++) { simpleAdapter[i] = remoteCalls.get(i).getRemoteContact(); } builder.setSingleChoiceItems(simpleAdapter , -1, new Dialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (service != null) { try { // 1 = PJSUA_XFER_NO_REQUIRE_REPLACES service.xferReplace(call.getCallId(), remoteCalls.get(which).getCallId(), 1); } catch (RemoteException e) { Log.e(THIS_FILE, "Was not able to call service method", e); } } dialog.dismiss(); } }) .setCancelable(true) .setNeutralButton(R.string.cancel, new Dialog.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .show(); } break; } case ADD_CALL: { Intent pickupIntent = new Intent(this, PickupSipUri.class); startActivityForResult(pickupIntent, PICKUP_SIP_URI_NEW_CALL); break; } case START_RECORDING :{ if(service != null) { // TODO : add a tweaky setting for two channel recording in different files. // Would just result here in two calls to start recording with different bitmask service.startRecording(call.getCallId(), SipManager.BITMASK_ALL); } break; } case STOP_RECORDING : { if(service != null) { service.stopRecording(call.getCallId()); } break; } case START_VIDEO : case STOP_VIDEO : { if(service != null) { Bundle opts = new Bundle(); opts.putBoolean(SipCallSession.OPT_CALL_VIDEO, whichAction == START_VIDEO); service.updateCallOptions(call.getCallId(), opts); } break; } case ZRTP_TRUST : { if(service != null) { service.zrtpSASVerified(call.getCallId()); } break; } case ZRTP_REVOKE : { if(service != null) { service.zrtpSASRevoke(call.getCallId()); } break; } } } catch (RemoteException e) { Log.e(THIS_FILE, "Was not able to call service method", e); } } @Override public void onLeftRightChoice(int whichHandle) { switch (whichHandle) { case LEFT_HANDLE: Log.d(THIS_FILE, "We unlock"); proximityManager.release(0); proximityManager.restartTimer(); break; case RIGHT_HANDLE: Log.d(THIS_FILE, "We clear the call"); onTrigger(IOnCallActionTrigger.TERMINATE_CALL, getActiveCallInfo()); proximityManager.release(0); default: break; } } /* // Drag and drop feature private Timer draggingTimer; public class OnBadgeTouchListener implements OnTouchListener { private SipCallSession call; private InCallCard badge; private boolean isDragging = false; private SetDraggingTimerTask draggingDelayTask; Vibrator vibrator; int beginX = 0; int beginY = 0; private class SetDraggingTimerTask extends TimerTask { @Override public void run() { vibrator.vibrate(50); setDragging(true); Log.d(THIS_FILE, "Begin dragging"); } }; public OnBadgeTouchListener(InCallCard aBadge, SipCallSession aCall) { call = aCall; badge = aBadge; vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // TODO : move somewhere else if (draggingTimer == null) { draggingTimer = new Timer("Dragging-timer"); } } @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); int X = (int) event.getRawX(); int Y = (int) event.getRawY(); // Reset the not proximity sensor lock overlay proximityManager.restartTimer(); switch (action) { case MotionEvent.ACTION_DOWN: if (draggingDelayTask != null) { draggingDelayTask.cancel(); } draggingDelayTask = new SetDraggingTimerTask(); beginX = X; beginY = Y; draggingTimer.schedule(draggingDelayTask, DRAGGING_DELAY); case MotionEvent.ACTION_MOVE: if (isDragging) { float size = Math.max(75.0f, event.getSize() + 50.0f); Rect wrap = new Rect( (int) (X - (size)), (int) (Y - (size)), (int) (X + (size / 2.0f)), (int) (Y + (size / 2.0f))); badge.bringToFront(); // Log.d(THIS_FILE, "Is moving to "+X+", "+Y); return true; } else { if (Math.abs(X - beginX) > 50 || Math.abs(Y - beginY) > 50) { Log.d(THIS_FILE, "Stop dragging"); stopDragging(); return true; } return false; } case MotionEvent.ACTION_UP: onDropBadge(X, Y, badge, call); stopDragging(); return true; // Yes we continue cause this is a stop action case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: Log.d(THIS_FILE, "Stop dragging"); stopDragging(); return false; } return false; } private void stopDragging() { // TODO : thread save it if (draggingDelayTask != null) { draggingDelayTask.cancel(); } setDragging(false); } private void setDragging(boolean dragging) { isDragging = dragging; DraggingInfo di = new DraggingInfo(isDragging, badge, call); runOnUiThread(new UpdateDraggingRunnable(di)); } public void setCallState(SipCallSession callInfo) { Log.d(THIS_FILE, "Updated call infos : " + call.getCallState() + " and " + call.getMediaStatus() + " et " + call.isLocalHeld()); call = callInfo; } } private Rect getViewRect(int id) { View v = findViewById(id); if(v != null && v.getVisibility() == View.VISIBLE) { return new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); } return null; } private void onDropBadge(int X, int Y, InCallCard badge, SipCallSession call) { Log.d(THIS_FILE, "Dropping !!! in " + X + ", " + Y); // Rectangle init if not already done if (endCallTargetRect == null) { endCallTargetRect = getViewRect(R.id.dropHangup); } if (holdTargetRect == null) { holdTargetRect = getViewRect(R.id.dropHold); } if (answerTargetRect == null) { answerTargetRect = getViewRect(R.id.dropAnswer); } if (xferTargetRect == null) { xferTargetRect = getViewRect(R.id.dropXfer); } // Rectangle matching if (endCallTargetRect != null && endCallTargetRect.contains(X, Y)) { // Drop in end call zone onTrigger(call.isIncoming() && call.isBeforeConfirmed() ? DECLINE_CALL : CLEAR_CALL, call); } else if (holdTargetRect != null && holdTargetRect.contains(X, Y)) { // check if not drop on held call boolean dropOnOtherCall = false; for (Entry<Integer, InCallInfo> badgeSet : badges.entrySet()) { Log.d(THIS_FILE, "On drop target searching for another badge"); int callId = badgeSet.getKey(); if (callId != call.getCallId()) { Log.d(THIS_FILE, "found a different badge than self"); SipCallSession callInfo = getCallInfo(callId); if (callInfo.isLocalHeld()) { Log.d(THIS_FILE, "Other badge is hold"); InCallInfo otherBadge = badgeSet.getValue(); Rect r = new Rect(otherBadge.getLeft(), otherBadge.getTop(), otherBadge.getRight(), otherBadge.getBottom()); Log.d(THIS_FILE, "Current X, Y " + X + ", " + Y + " -- " + r.top + ", " + r.left + ", " + r.right + ", " + r.bottom); if (r.contains(X, Y)) { Log.d(THIS_FILE, "Yep we've got one"); dropOnOtherCall = true; if (service != null) { try { // 1 = PJSUA_XFER_NO_REQUIRE_REPLACES service.xferReplace(call.getCallId(), callId, 1); } catch (RemoteException e) { // TODO : toaster } } } } } } // Drop in hold zone if (!dropOnOtherCall && !call.isLocalHeld()) { onTrigger(TOGGLE_HOLD, call); } } else if (answerTargetRect != null && answerTargetRect.contains(X, Y)) { if (call.isIncoming() && call.isBeforeConfirmed()) { onTrigger(TAKE_CALL, call); } } else if (xferTargetRect != null && xferTargetRect.contains(X, Y)) { if (!call.isBeforeConfirmed() && !call.isAfterEnded()) { onTrigger(XFER_CALL, call); } } else { Log.d(THIS_FILE, "Drop is done somewhere else " + call.getMediaStatus()); // Drop somewhere else if (call.isLocalHeld()) { Log.d(THIS_FILE, "Try to unhold"); onTrigger(TOGGLE_HOLD, call); } } runOnUiThread(new UpdateUIFromMediaRunnable()); } private class DraggingInfo { public boolean isDragging = false; // public InCallInfo2 badge; public SipCallSession call; public DraggingInfo(boolean aIsDragging, InCallCard aBadge, SipCallSession aCall) { isDragging = aIsDragging; // badge = aBadge; call = aCall; } } */ private class ShowZRTPInfoRunnable implements Runnable, DialogInterface.OnClickListener { private String sasString; private SipCallSession callSession; public ShowZRTPInfoRunnable(SipCallSession call, String sas) { callSession = call; sasString = sas; } @Override public void onClick(DialogInterface dialog, int which) { if(which == DialogInterface.BUTTON_POSITIVE) { Log.d(THIS_FILE, "ZRTP confirmed"); if (service != null) { try { service.zrtpSASVerified(callSession.getCallId()); } catch (RemoteException e) { Log.e(THIS_FILE, "Error while calling service", e); } dialog.dismiss(); } }else if(which == DialogInterface.BUTTON_NEGATIVE) { dialog.dismiss(); } } @Override public void run() { AlertDialog.Builder builder = new AlertDialog.Builder(InCallActivity.this); Resources r = getResources(); builder.setTitle("ZRTP supported by remote party"); builder.setMessage("Do you confirm the SAS : " + sasString); builder.setPositiveButton(r.getString(R.string.yes), this); builder.setNegativeButton(r.getString(R.string.no), this); AlertDialog backupDialog = builder.create(); backupDialog.show(); } } @Override public boolean shouldActivateProximity() { // TODO : missing headset & keyboard open if(lastMediaState != null) { if(lastMediaState.isBluetoothScoOn) { return false; } if(lastMediaState.isSpeakerphoneOn && ! useAutoDetectSpeaker) { // Imediate reason to not enable proximity sensor return false; } } if (callsInfo == null) { return false; } boolean isValidCallState = true; int count = 0; for (SipCallSession callInfo : callsInfo) { if(callInfo.mediaHasVideo()) { return false; } if(!callInfo.isAfterEnded()) { int state = callInfo.getCallState(); isValidCallState &= ( (state == SipCallSession.InvState.CONFIRMED) || (state == SipCallSession.InvState.CONNECTING) || (state == SipCallSession.InvState.CALLING) || (state == SipCallSession.InvState.EARLY && !callInfo.isIncoming()) ); count ++; } } if(count == 0) { return false; } return isValidCallState; } @Override public void onProximityTrackingChanged(boolean acquired) { if(useAutoDetectSpeaker && service != null) { if(acquired) { if(lastMediaState == null || lastMediaState.isSpeakerphoneOn) { try { service.setSpeakerphoneOn(false); } catch (RemoteException e) { Log.e(THIS_FILE, "Can't run speaker change"); } } }else { if(lastMediaState == null || !lastMediaState.isSpeakerphoneOn) { try { service.setSpeakerphoneOn(true); } catch (RemoteException e) { Log.e(THIS_FILE, "Can't run speaker change"); } } } } } // Active call adapter private class CallsAdapter extends BaseAdapter { private boolean mActiveCalls; private SparseArray<Long> seenConnected = new SparseArray<Long>(); public CallsAdapter(boolean notOnHold) { mActiveCalls = notOnHold; } private boolean isValidCallForAdapter(SipCallSession call) { boolean holdStateOk = false; if(mActiveCalls && !call.isLocalHeld()) { holdStateOk = true; } if(!mActiveCalls && call.isLocalHeld()) { holdStateOk = true; } if(holdStateOk) { long currentTime = System.currentTimeMillis(); if(call.isAfterEnded()) { // Only valid if we already seen this call in this adapter to be valid if(hasNoMoreActiveCall() && seenConnected.get(call.getCallId(), currentTime + 2 * QUIT_DELAY) < currentTime + QUIT_DELAY) { return true; }else { seenConnected.delete(call.getCallId()); return false; } }else { seenConnected.put(call.getCallId(), currentTime); return true; } } return false; } private boolean hasNoMoreActiveCall() { synchronized (callMutex) { if(callsInfo == null) { return true; } for(SipCallSession call : callsInfo) { // As soon as we have one not after ended, we have at least active call if(!call.isAfterEnded()) { return false; } } } return true; } @Override public int getCount() { int count = 0; synchronized (callMutex) { if(callsInfo == null) { return 0; } for(SipCallSession call : callsInfo) { if(isValidCallForAdapter(call)) { count ++; } } } return count; } @Override public Object getItem(int position) { synchronized (callMutex) { if(callsInfo == null) { return null; } int count = 0; for(SipCallSession call : callsInfo) { if(isValidCallForAdapter(call)) { if(count == position) { return call; } count ++; } } } return null; } @Override public long getItemId(int position) { SipCallSession call = (SipCallSession) getItem(position); if(call != null) { return call.getCallId(); } return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { if(convertView == null) { convertView = new InCallCard(InCallActivity.this, null); } if(convertView instanceof InCallCard) { InCallCard vc = (InCallCard) convertView; vc.setOnTriggerListener(InCallActivity.this); // TODO --- //badge.setOnTouchListener(new OnBadgeTouchListener(badge, call)); SipCallSession session = (SipCallSession) getItem(position); vc.setCallState(session); } return convertView; } } }