/**
 * 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.webkit.WebView;
import android.webkit.WebViewClient;
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 TAG = "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.i(TAG, "######## onCreate");
        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(TAG, "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(TAG, "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(TAG, "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(TAG, "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(TAG, "New intent is launched");
        super.onNewIntent(intent);
    }
    
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.d(TAG, "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) {
                    Log.d(TAG, "OnActivityResult, PICKUP_SIP_URI_NEW_CALL");
                    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
     * 
     * @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(TAG,
                                "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(TAG, "Active call is " + mainCallInfo.getCallId());
                Log.d(TAG, "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(TAG, "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(TAG, "Active call session is disconnected or null wait for quit...");
                        // This will release locks
                        onDisplayVideo(false);
                        delayedQuit();
                        return;
    
                }
                
                Log.d(TAG, "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(TAG, "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(TAG, "Releasing wake up lock");
            wakeLock.release();
        }
        
        proximityManager.release(0);
        
        activeCallsGrid.setVisibility(View.VISIBLE);
        inCallControls.setVisibility(View.GONE);

        Log.d(TAG, "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(TAG, "Run quit timer");
            finish();
        }
    }

    private void showDialpad(int callId) {
        Log.i(TAG, "showDialpad");
        DtmfDialogFragment newFragment = DtmfDialogFragment.newInstance(callId);
        newFragment.show(getSupportFragmentManager(), "dialog");
    }


    @Override
    public void OnDtmf(int callId, int keyCode, int dialTone) {
        Log.i(TAG, "onDtmf, keyCode: " + keyCode);

        proximityManager.restartTimer();

        if (service != null) {
            if (callId != SipCallSession.INVALID_CALL_ID) {
                try {
                    service.sendDtmf(callId, keyCode);
                    dialFeedback.giveFeedback(dialTone);
                } catch (RemoteException e) {
                    Log.e(TAG, "Was not able to send dtmf tone", e);
                }
            }
        }
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.d(TAG, "Key down : " + keyCode);
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
            case KeyEvent.KEYCODE_VOLUME_UP:
                //
                // Volume has been adjusted by the user.
                //
                Log.d(TAG, "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;
                }

                //TODO: adjust volume here
                if (service != null) {
                    try {
                        service.adjustVolume(currentCallInfo, action, AudioManager.FLAG_SHOW_UI);
                    } catch (RemoteException e) {
                        Log.e(TAG, "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(TAG, "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(TAG, "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(TAG, "Media update ...." + mediaState.isSpeakerphoneOn);
                        synchronized (callMutex) {
                            if (!mediaState.equals(lastMediaState)) {
                                lastMediaState = mediaState;
                                runOnUiThread(new UpdateUIFromMediaRunnable());
                            }   
                        }
                    } catch (RemoteException e) {
                        Log.e(TAG, "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(TAG,
                // "Service started get real call info "+callInfo.getCallId());
                callsInfo = service.getCalls();
                serviceConnected = true;

                runOnUiThread(new UpdateUIFromCallRunnable());
                runOnUiThread(new UpdateUIFromMediaRunnable());
            } catch (RemoteException e) {
                Log.e(TAG, "Can't get back the call", e);
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            serviceConnected = false;
            callsInfo = null;
        }
    };
    private AlertDialog infoDialog;

    // private boolean showDetails = true;


    /**
     * Controls buttons (accept, reject etc).
     * @param whichAction what action has been done
     * @param call
     */
    @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(TAG, "Try to do an action on a null call !!!");
                return;
            }
            if (call.getCallId() == SipCallSession.INVALID_CALL_ID) {
                Log.e(TAG, "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.i(TAG, "Answering 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(TAG, "Hold call " + callInfo.getCallId());
                                    service.hold(callInfo.getCallId());

                                }
                            }
                        }
                    }
                    break;
                }
                case DONT_TAKE_CALL: {
                    if (service != null) {
                        Log.i(TAG, "Rejecting the call with BUSY_HERE");
                        service.hangup(call.getCallId(), StatusCode.BUSY_HERE);
                    }
                    break;
                }
                case REJECT_CALL:
                case TERMINATE_CALL: {
                    if (service != null) {
                        Log.i(TAG, "Rejecting the call");
                        service.hangup(call.getCallId(), 0);
                    }
                    break;
                }
                case MUTE_ON:
                case MUTE_OFF: {
                    if (service != null) {
                        Log.i(TAG, "Set mute " + (whichAction == MUTE_ON));
                        service.setMicrophoneMute((whichAction == MUTE_ON) ? true : false);
                    }
                    break;
                }
                case SPEAKER_ON:
                case SPEAKER_OFF: {
                    if (service != null) {
                        Log.d(TAG, "Set speaker " + (whichAction == SPEAKER_ON));
                        useAutoDetectSpeaker = false;
                        service.setSpeakerphoneOn((whichAction == SPEAKER_ON) ? true : false);
                    }
                    break;
                }
                case BLUETOOTH_ON:
                case BLUETOOTH_OFF: {
                    if (service != null) {
                        Log.d(TAG, "Set bluetooth " + (whichAction == BLUETOOTH_ON));
                        service.setBluetoothOn((whichAction == BLUETOOTH_ON) ? true : false);
                    }
                    break;
                }
                case DTMF_DISPLAY: {
                    Log.d(TAG, "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(TAG,
                        // "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(TAG, "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(TAG, "Was not able to call service method", e);
        }
    }
    


    @Override
    public void onLeftRightChoice(int whichHandle) {
        switch (whichHandle) {
            case LEFT_HANDLE:
                Log.d(TAG, "We unlock");
                proximityManager.release(0);
                proximityManager.restartTimer();
                break;
            case RIGHT_HANDLE:
                Log.d(TAG, "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(TAG, "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(TAG, "Is moving to "+X+", "+Y);
                        return true;
                    } else {
                        if (Math.abs(X - beginX) > 50 || Math.abs(Y - beginY) > 50) {
                            Log.d(TAG, "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(TAG, "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(TAG,
                    "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(TAG, "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(TAG, "On drop target searching for another badge");
                int callId = badgeSet.getKey();
                if (callId != call.getCallId()) {
                    Log.d(TAG, "found a different badge than self");
                    SipCallSession callInfo = getCallInfo(callId);
                    if (callInfo.isLocalHeld()) {
                        Log.d(TAG, "Other badge is hold");
                        InCallInfo otherBadge = badgeSet.getValue();
                        Rect r = new Rect(otherBadge.getLeft(), otherBadge.getTop(),
                                otherBadge.getRight(), otherBadge.getBottom());
                        Log.d(TAG, "Current X, Y " + X + ", " + Y + " -- " + r.top + ", "
                                + r.left + ", " + r.right + ", " + r.bottom);
                        if (r.contains(X, Y)) {
                            Log.d(TAG, "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(TAG, "Drop is done somewhere else " + call.getMediaStatus());
            // Drop somewhere else
            if (call.isLocalHeld()) {
                Log.d(TAG, "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(TAG, "ZRTP confirmed");
                if (service != null) {
                    try {
                        service.zrtpSASVerified(callSession.getCallId());
                    } catch (RemoteException e) {
                        Log.e(TAG, "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(TAG, "Can't run speaker change");
                    }
                }
            }else {
                if(lastMediaState == null || !lastMediaState.isSpeakerphoneOn) {
                    try {
                        service.setSpeakerphoneOn(true);
                    } catch (RemoteException e) {
                        Log.e(TAG, "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;
        }
        
    }



}
