com.csipsimple.ui.incall.CallActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.csipsimple.ui.incall.CallActivity.java

Source

/**
 * 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.support.v4.app.FragmentActivity;
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.view.Window;
import android.widget.BaseAdapter;
import android.widget.RelativeLayout;
import android.widget.Toast;

import com.csipsimple.R;
import com.csipsimple.api.ISipService;
import com.csipsimple.api.MakeCallService;
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;

import eu.miraculouslife.android.csipsimple.apilib.ApiConstants;

public class CallActivity extends FragmentActivity
        implements IOnCallActionTrigger, IOnLeftRightChoice, ProximityDirector, OnDtmfListener {
    private static final int QUIT_DELAY = 1000;
    private final static String TAG = CallActivity.class.getSimpleName();
    //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";

    private String targetName = "";

    private boolean callEnded = false;
    private boolean callConnectedBroadcastSent = false;
    private boolean callIncomingSent = false;

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //handler.setActivityInstance(this);
        Log.i(TAG, "######## onCreate");
        getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        this.setFinishOnTouchOutside(false);
        setContentView(R.layout.call_dialog);

        targetName = getIntent().getStringExtra(SipManager.CALLEE_NAME_INTENT_KEY);
        if (targetName != null) {
            Log.i(TAG, "targetName: " + targetName);
        }

        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() {
        Log.i(TAG, "onDestroy");
        sendEndCallBroadcast();

        callEnded = false;
        callConnectedBroadcastSent = false;
        callIncomingSent = false;

        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, CallActivity.this));
                int state = mainCallInfo.getCallState();

                //int backgroundResId = R.drawable.bg_in_call_gradient_unidentified;

                // We manage wake lock
                switch (state) {
                case SipCallSession.InvState.INCOMING:
                    Log.i(TAG, "SipCallSession.InvState.INCOMING");
                case SipCallSession.InvState.EARLY:
                    Log.i(TAG, "SipCallSession.InvState.EARLY");
                case SipCallSession.InvState.CALLING:
                    Log.i(TAG, "SipCallSession.InvState.CALLING");
                case SipCallSession.InvState.CONNECTING:
                    Log.i(TAG, "SipCallSession.InvState.CONNECTING");
                    Log.d(TAG, "Acquire wake up lock");
                    if (wakeLock != null && !wakeLock.isHeld()) {
                        wakeLock.acquire();
                    }

                    callEnded = false;

                    /*
                    If it is an incoming call, broadcast a message to inform the UI and the ML
                    System about it.
                     */
                    if (mainCallInfo.isIncoming()) {
                        sendIncomingCallBroadcast(mainCallInfo.getRemoteContact());
                    }

                    break;
                case SipCallSession.InvState.CONFIRMED:
                    Log.i(TAG, "SipCallSession.InvState.CONFIRMED - (Call connected)");

                    callConnectedBroadcastSent = false;

                    Log.i(TAG, "remoteContact: " + mainCallInfo.getRemoteContact());
                    Log.w(TAG, "is incoming: " + mainCallInfo.isIncoming());
                    Log.i(TAG, "call info: " + mainCallInfo.toString());
                    Log.i(TAG, "getCallStart: " + mainCallInfo.getCallStart());
                    Log.i(TAG, "getConnectStart: " + mainCallInfo.getConnectStart());

                    Toast.makeText(CallActivity.this, "Call connected", Toast.LENGTH_SHORT).show();
                    sendCallConnectedBroadcast(mainCallInfo.isIncoming(), mainCallInfo.getRemoteContact());

                    break;
                case SipCallSession.InvState.NULL:
                    Log.i(TAG, "SipCallSession.InvState.NULL");
                case SipCallSession.InvState.DISCONNECTED:
                    Log.i(TAG, "SipCallSession.InvState.DISCONNECTED");
                    Log.d(TAG, "Active call session is disconnected or null wait for quit...");
                    Log.i(TAG, "Set call as ended");

                    callIncomingSent = false;
                    sendEndCallBroadcast();
                    // This will release locks
                    onDisplayVideo(false);
                    delayedQuit();
                    return;
                }

                Log.d(TAG, "we leave the update ui function");
            }

            proximityManager.updateProximitySensorMode();

            if (heldsCalls + mainsCalls == 0) {
                delayedQuit();
            }
        }
    }

    private void sendIncomingCallBroadcast(String calleeContact) {
        Log.i(TAG, "sendIncomingCallBroadcast");
        if (!callIncomingSent) {
            Intent intent = new Intent();
            intent.putExtra(ApiConstants.API_RESPONSE_TYPE_INTENT_KEY,
                    ApiConstants.API_RESPONSE_TYPE_INCOMING_CALL);
            intent.putExtra(ApiConstants.CALL_INCOMING_CALLEE_INTENT_KEY, calleeContact);
            intent.setAction(ApiConstants.API_RESPONSE_BROADCAST_ACTION);
            sendBroadcast(intent);
            callIncomingSent = true;
        } else {
            Log.i(TAG, "incoming call already sent, not sending incoming call broadcast");
        }
    }

    private void sendCallConnectedBroadcast(boolean isIncoming, String calleeContact) {
        Log.i(TAG, "sendCallConnectedBroadcast");

        MakeCallService.CALLEE_NAME = "";
        Intent intent = new Intent();
        intent.putExtra(ApiConstants.API_RESPONSE_TYPE_INTENT_KEY, ApiConstants.API_RESPONSE_TYPE_CALL_CONNECTED);
        intent.putExtra(ApiConstants.IS_CALL_INCOMING_INTENT_KEY, isIncoming);
        intent.putExtra(ApiConstants.CALL_CONNECTED_CALLEE_INTENT_KEY, calleeContact);
        intent.setAction(ApiConstants.API_RESPONSE_BROADCAST_ACTION);
        Timer timer = new Timer();

        // wait for two seconds, then check if the call is still active
        // otherwise, we consider it as not connected
        timer.schedule(new SendConnectedTask(intent), 2000);
    }

    private void sendEndCallBroadcast() {
        if (!callEnded) {
            Log.i(TAG, "sendEndCallBroadcast");
            MakeCallService.CALLEE_NAME = "";
            Intent intent = new Intent();
            intent.putExtra(ApiConstants.API_RESPONSE_TYPE_INTENT_KEY, ApiConstants.API_RESPONSE_TYPE_CALL_ENDED);
            intent.setAction(ApiConstants.API_RESPONSE_BROADCAST_ACTION);
            sendBroadcast(intent);
            callEnded = true;
        } else {
            Log.i(TAG, "call already ended, not sending end call broadcast");
        }
    }

    private void sendCancelCallBroadcast() {
        if (!callEnded) {
            Log.i(TAG, "sendCancelCallBroadcast");
            MakeCallService.CALLEE_NAME = "";
            Intent intent = new Intent();
            intent.putExtra(ApiConstants.API_RESPONSE_TYPE_INTENT_KEY,
                    ApiConstants.API_RESPONSE_TYPE_CALL_CANCELLED);
            intent.setAction(ApiConstants.API_RESPONSE_BROADCAST_ACTION);
            sendBroadcast(intent);
            callEnded = true;
        } else {
            Log.i(TAG, "call already ended, not sending cancel call broadcast");
        }
    }

    @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(), 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:
                if (service != null) {
                    Log.i(TAG, "Rejecting the call");
                    service.hangup(call.getCallId(), 0);
                }
                break;
            case TERMINATE_CALL: {
                if (service != null) {
                    Log.i(TAG, "Terminating the call");
                    service.hangup(call.getCallId(), 0);
                }
                break;
            }
            case CANCEL_CALL: {
                if (service != null) {
                    Log.i(TAG, "Cancelling the call");
                    service.hangup(call.getCallId(), 0);
                }
                sendCancelCallBroadcast();
                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 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 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;
        }

    }

    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() {
            Builder builder = new Builder(CallActivity.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) {
                if (targetName != null && !targetName.isEmpty()) {
                    convertView = new InCallCard(CallActivity.this, null, targetName);
                } else {
                    convertView = new InCallCard(CallActivity.this, null);
                }
            }

            if (convertView instanceof InCallCard) {
                InCallCard vc = (InCallCard) convertView;
                vc.setOnTriggerListener(CallActivity.this);
                // TODO ---
                //badge.setOnTouchListener(new OnBadgeTouchListener(badge, call));

                SipCallSession session = (SipCallSession) getItem(position);
                if (targetName != null && !targetName.isEmpty()) {
                    vc.setCallState(session, targetName);
                } else {
                    vc.setCallState(session);
                }
            }

            return convertView;
        }
    }

    class SendConnectedTask extends TimerTask {
        private Intent intent;

        public SendConnectedTask(Intent intent) {
            this.intent = intent;
        }

        public void run() {
            try {
                Log.d(TAG, "Time's up!");
                if (!callConnectedBroadcastSent) {
                    if (!callEnded) {
                        Log.d(TAG, "call is still active, sending broadcast");
                        sendBroadcast(intent);
                    } else {
                        Log.i(TAG, "call is not active any more, cancelling broadcast");
                    }
                    callConnectedBroadcastSent = true;
                } else {
                    Log.i(TAG, "call end broadcast already sent, dismissing this one");
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception while executing SendConnectedTask", e);
            }
        }
    }
}