com.mindprotectionkit.freephone.RedPhoneService.java Source code

Java tutorial

Introduction

Here is the source code for com.mindprotectionkit.freephone.RedPhoneService.java

Source

/*
 * Copyright (C) 2011 Whisper Systems
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mindprotectionkit.freephone;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.telephony.TelephonyManager;
import android.util.Log;

import org.thoughtcrime.redphone.audio.IncomingRinger;
import org.thoughtcrime.redphone.audio.OutgoingRinger;
import com.mindprotectionkit.freephone.call.CallManager;
import com.mindprotectionkit.freephone.call.CallStateListener;
import com.mindprotectionkit.freephone.call.InitiatingCallManager;
import com.mindprotectionkit.freephone.call.LockManager;
import com.mindprotectionkit.freephone.call.ResponderCallManager;
import org.thoughtcrime.redphone.codec.CodecSetupException;
import com.mindprotectionkit.freephone.contacts.PersonInfo;
import com.mindprotectionkit.freephone.crypto.zrtp.SASInfo;
import com.mindprotectionkit.freephone.gcm.GCMRegistrarHelper;
import com.mindprotectionkit.freephone.monitor.CallDataImpl;
import com.mindprotectionkit.freephone.pstn.CallStateView;
import com.mindprotectionkit.freephone.pstn.IncomingPstnCallListener;
import com.mindprotectionkit.freephone.signaling.OtpCounterProvider;
import com.mindprotectionkit.freephone.signaling.SessionDescriptor;
import com.mindprotectionkit.freephone.signaling.SignalingException;
import com.mindprotectionkit.freephone.signaling.SignalingSocket;
import com.mindprotectionkit.freephone.ui.ApplicationPreferencesActivity;
import com.mindprotectionkit.freephone.ui.CallQualityDialog;
import com.mindprotectionkit.freephone.ui.NotificationBarManager;
import com.mindprotectionkit.freephone.util.Base64;
import com.mindprotectionkit.freephone.util.CallLogger;
import com.mindprotectionkit.freephone.util.UncaughtExceptionHandlerManager;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;

/**
 * The major entry point for all of the heavy lifting associated with
 * setting up, tearing down, or managing calls.  The service spins up
 * either from a broadcast listener that has detected an incoming call,
 * or from a UI element that wants to initiate an outgoing call.
 *
 * @author Moxie Marlinspike
 *
 */
public class RedPhoneService extends Service implements CallStateListener, CallStateView {
    public static final String ACTION_INCOMING_CALL = "org.thoughtcrime.redphone.RedPhoneService.INCOMING_CALL";
    public static final String ACTION_OUTGOING_CALL = "org.thoughtcrime.redphone.RedPhoneService.OUTGOING_CALL";
    public static final String ACTION_ANSWER_CALL = "org.thoughtcrime.redphone.RedPhoneService.ANSWER_CALL";
    public static final String ACTION_DENY_CALL = "org.thoughtcrime.redphone.RedPhoneService.DENYU_CALL";
    public static final String ACTION_HANGUP_CALL = "org.thoughtcrime.redphone.RedPhoneService.HANGUP";
    public static final String ACTION_SET_MUTE = "org.thoughtcrime.redphone.RedPhoneService.SET_MUTE";
    public static final String ACTION_CONFIRM_SAS = "org.thoughtcrime.redphone.RedPhoneService.CONFIRM_SAS";

    private static final String TAG = RedPhoneService.class.getName();

    private final List<Message> bufferedEvents = new LinkedList<Message>();
    private final IBinder binder = new RedPhoneServiceBinder();
    private final Handler serviceHandler = new Handler();

    private OutgoingRinger outgoingRinger;
    private IncomingRinger incomingRinger;

    private int state;
    private byte[] zid;
    private String localNumber;
    private String remoteNumber;
    private String password;
    private CallManager currentCallManager;
    private LockManager lockManager;
    private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager;

    private Handler handler;
    private CallLogger.CallRecord currentCallRecord;
    private IncomingPstnCallListener pstnCallListener;

    @Override
    public void onCreate() {
        super.onCreate();

        if (Release.DEBUG)
            Log.w("RedPhoneService", "Service onCreate() called...");

        initializeResources();
        initializeApplicationContext();
        initializeRingers();
        initializePstnCallListener();
        registerUncaughtExceptionHandler();

        CallDataImpl.clearCache(this);
    }

    @Override
    public void onStart(Intent intent, int startId) {
        if (Release.DEBUG)
            Log.w("RedPhoneService", "Service onStart() called...");
        if (intent == null)
            return;
        new Thread(new IntentRunnable(intent)).start();

        GCMRegistrarHelper.registerClient(this, false);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(pstnCallListener);
        NotificationBarManager.setCallEnded(this);
        uncaughtExceptionHandlerManager.unregister();
    }

    private synchronized void onIntentReceived(Intent intent) {
        Log.w("RedPhoneService", "Received Intent: " + intent.getAction());

        if (intent.getAction().equals(ACTION_INCOMING_CALL) && isBusy())
            handleBusyCall(intent);
        else if (intent.getAction().equals(ACTION_INCOMING_CALL))
            handleIncomingCall(intent);
        else if (intent.getAction().equals(ACTION_OUTGOING_CALL) && isIdle())
            handleOutgoingCall(intent);
        else if (intent.getAction().equals(ACTION_ANSWER_CALL))
            handleAnswerCall(intent);
        else if (intent.getAction().equals(ACTION_DENY_CALL))
            handleDenyCall(intent);
        else if (intent.getAction().equals(ACTION_HANGUP_CALL))
            handleHangupCall(intent);
        else if (intent.getAction().equals(ACTION_SET_MUTE))
            handleSetMute(intent);
        else if (intent.getAction().equals(ACTION_CONFIRM_SAS))
            handleConfirmSas(intent);
    }

    ///// Initializers

    private void initializeRingers() {
        this.outgoingRinger = new OutgoingRinger(this);
        this.incomingRinger = new IncomingRinger(this);
    }

    private void initializePstnCallListener() {
        pstnCallListener = new IncomingPstnCallListener(this);
        registerReceiver(pstnCallListener, new IntentFilter("android.intent.action.PHONE_STATE"));
    }

    private void initializeApplicationContext() {
        ApplicationContext context = ApplicationContext.getInstance();
        context.setContext(this);
        context.setCallStateListener(this);
    }

    private void initializeResources() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        this.state = RedPhone.STATE_IDLE;
        this.zid = getZID();
        this.localNumber = preferences.getString(Constants.NUMBER_PREFERENCE, "NO_SAVED_NUMBER!");
        this.password = preferences.getString(Constants.PASSWORD_PREFERENCE, "NO_SAVED_PASSWORD!");

        this.lockManager = new LockManager(this);
    }

    private void registerUncaughtExceptionHandler() {
        uncaughtExceptionHandlerManager = new UncaughtExceptionHandlerManager();
        uncaughtExceptionHandlerManager.registerHandler(new ProximityLockRelease(lockManager));
    }

    /// Intent Handlers

    private void handleIncomingCall(Intent intent) {
        SessionDescriptor session = (SessionDescriptor) intent.getParcelableExtra(Constants.SESSION);
        remoteNumber = extractRemoteNumber(intent);
        state = RedPhone.STATE_RINGING;

        lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
        this.currentCallManager = new ResponderCallManager(this, this, remoteNumber, localNumber, password, session,
                zid);
        this.currentCallManager.start();
    }

    private void handleOutgoingCall(Intent intent) {
        remoteNumber = extractRemoteNumber(intent);

        if (remoteNumber == null || remoteNumber.length() == 0)
            return;

        sendMessage(RedPhone.HANDLE_OUTGOING_CALL, remoteNumber);

        state = RedPhone.STATE_DIALING;
        lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
        this.currentCallManager = new InitiatingCallManager(this, this, localNumber, password, remoteNumber, zid);
        this.currentCallManager.start();

        NotificationBarManager.setCallInProgress(this);

        currentCallRecord = CallLogger.logOutgoingCall(this, remoteNumber);
    }

    private void handleBusyCall(Intent intent) {
        SessionDescriptor session = (SessionDescriptor) intent.getParcelableExtra(Constants.SESSION);

        if (currentCallManager != null && session.equals(currentCallManager.getSessionDescriptor())) {
            Log.w("RedPhoneService", "Duplicate incoming call signal, ignoring...");
            return;
        }

        handleMissedCall(extractRemoteNumber(intent));

        try {
            SignalingSocket signalingSocket = new SignalingSocket(this, session.getFullServerName(),
                    Release.SERVER_PORT, localNumber, password, OtpCounterProvider.getInstance());

            signalingSocket.setBusy(session.sessionId);
            signalingSocket.close();
        } catch (SignalingException e) {
            Log.w("RedPhoneService", e);
        }
    }

    private void handleMissedCall(String remoteNumber) {
        CallLogger.logMissedCall(this, remoteNumber, System.currentTimeMillis());
        NotificationBarManager.notifyMissedCall(this, remoteNumber);
    }

    private void handleAnswerCall(Intent intent) {
        state = RedPhone.STATE_ANSWERING;
        incomingRinger.stop();
        currentCallRecord = CallLogger.logIncomingCall(this, remoteNumber);
        if (currentCallManager != null) {
            ((ResponderCallManager) this.currentCallManager).answer(true);
        }
    }

    private void handleDenyCall(Intent intent) {
        state = RedPhone.STATE_IDLE;
        incomingRinger.stop();
        CallLogger.logMissedCall(this, remoteNumber, System.currentTimeMillis());
        if (currentCallManager != null) {
            ((ResponderCallManager) this.currentCallManager).answer(false);
        }
        this.terminate();
    }

    private void handleHangupCall(Intent intent) {
        this.terminate();
    }

    private void handleSetMute(Intent intent) {
        if (currentCallManager != null) {
            currentCallManager.setMute(intent.getBooleanExtra(Constants.MUTE_VALUE, false));
        }
    }

    private void handleConfirmSas(Intent intent) {
        if (currentCallManager != null)
            currentCallManager.setSasVerified();
    }

    /// Helper Methods

    private boolean isBusy() {
        TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        return ((currentCallManager != null && state != RedPhone.STATE_IDLE)
                || telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE);
    }

    private boolean isIdle() {
        return state == RedPhone.STATE_IDLE;
    }

    private void shutdownAudio() {
        AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
        am.setMode(AudioManager.MODE_NORMAL);
    }

    public int getState() {
        return state;
    }

    public SASInfo getCurrentCallSAS() {
        if (currentCallManager != null)
            return currentCallManager.getSasInfo();
        else
            return null;
    }

    public PersonInfo getRemotePersonInfo() {
        return PersonInfo.getInstance(this, remoteNumber);
    }

    private byte[] getZID() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        if (preferences.contains("ZID")) {
            try {
                return Base64.decode(preferences.getString("ZID", null));
            } catch (IOException e) {
                return setZID();
            }
        } else {
            return setZID();
        }
    }

    private byte[] setZID() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        try {
            byte[] zid = new byte[12];
            SecureRandom.getInstance("SHA1PRNG").nextBytes(zid);
            String encodedZid = Base64.encodeBytes(zid);

            preferences.edit().putString("ZID", encodedZid).commit();

            return zid;
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private String extractRemoteNumber(Intent i) {
        String number = i.getStringExtra(Constants.REMOTE_NUMBER);

        if (number == null)
            number = i.getData().getSchemeSpecificPart();

        if (number.endsWith("*"))
            return number.substring(0, number.length() - 1);
        else
            return number;
    }

    private void startCallCardActivity() {
        Intent activityIntent = new Intent();
        activityIntent.setClass(this, RedPhone.class);
        activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(activityIntent);
    }

    private synchronized void terminate() {
        Log.w("RedPhoneService", "termination stack", new Exception());
        lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
        NotificationBarManager.setCallEnded(this);

        incomingRinger.stop();
        outgoingRinger.stop();

        if (currentCallRecord != null) {
            currentCallRecord.finishCall();
            currentCallRecord = null;
        }

        if (currentCallManager != null) {
            maybeStartQualityMetricsActivity();
            currentCallManager.terminate();
            currentCallManager = null;
        }

        shutdownAudio();

        state = RedPhone.STATE_IDLE;
        lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
        // XXX moxie@thoughtcrime.org -- Do we still need to stop the Service?
        //    Log.d("RedPhoneService", "STOP SELF" );
        //    this.stopSelf();
    }

    public void maybeStartQualityMetricsActivity() {
        if (currentCallManager.getSessionDescriptor() == null || !currentCallManager.callConnected()
                || (!ApplicationPreferencesActivity.getDisplayDialogPreference(this)
                        && ApplicationPreferencesActivity.wasUserNotifedOfCallQaulitySettings(this))) {
            return;
        }

        SessionDescriptor sessionDescriptor = currentCallManager.getSessionDescriptor();
        Intent callQualityDialogIntent = new Intent(this, CallQualityDialog.class);
        callQualityDialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        callQualityDialogIntent.putExtra("callId", sessionDescriptor.sessionId);
        startActivity(callQualityDialogIntent);

        Notification notification = new NotificationCompat.Builder(this).setAutoCancel(true)
                .setContentTitle(getResources().getText(R.string.CallQualityDialog__redphone))
                .setContentText(getResources().getText(R.string.CallQualityDialog__provide_call_quality_feedback))
                .setContentIntent(PendingIntent.getActivity(this, 0, callQualityDialogIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT))
                .setSmallIcon(R.drawable.registration_notification).setDefaults(Notification.DEFAULT_LIGHTS)
                .setTicker(getResources().getText(R.string.CallQualityDialog__provide_call_quality_feedback))
                .build();

        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.notify(CallQualityDialog.CALL_QUALITY_NOTIFICATION_ID, notification);
    }

    public void setCallStateHandler(Handler handler) {
        this.handler = handler;

        if (handler != null) {
            for (Message message : bufferedEvents) {
                handler.sendMessage(message);
            }

            bufferedEvents.clear();
        }
    }

    ///////// CallStateListener Implementation

    public void notifyCallStale() {
        Log.w("RedPhoneService", "Got a stale call, probably an old SMS...");
        handleMissedCall(remoteNumber);
        this.terminate();
    }

    public void notifyCallFresh() {
        Log.w("RedPhoneService", "Good call, time to ring and display call card...");
        sendMessage(RedPhone.HANDLE_INCOMING_CALL, remoteNumber);

        lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE);

        startCallCardActivity();
        incomingRinger.start();

        NotificationBarManager.setCallInProgress(this);
    }

    public void notifyBusy() {
        Log.w("RedPhoneService", "Got busy signal from responder!");
        sendMessage(RedPhone.HANDLE_CALL_BUSY, null);
        outgoingRinger.playBusy();
        serviceHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                RedPhoneService.this.terminate();
            }
        }, RedPhone.BUSY_SIGNAL_DELAY_FINISH);
    }

    public void notifyCallRinging() {
        outgoingRinger.playRing();
        sendMessage(RedPhone.HANDLE_CALL_RINGING, null);
    }

    public void notifyCallConnected(SASInfo sas) {
        outgoingRinger.playComplete();
        lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL);
        state = RedPhone.STATE_CONNECTED;
        synchronized (this) {
            sendMessage(RedPhone.HANDLE_CALL_CONNECTED, sas);
            try {
                wait();
            } catch (InterruptedException e) {
                throw new AssertionError("Wait interrupted in RedPhoneService");
            }
        }
    }

    public void notifyCallConnectionUIUpdateComplete() {
        synchronized (this) {
            this.notify();
        }
    }

    public void notifyDebugInfo(String info) {
        sendMessage(RedPhone.HANDLE_DEBUG_INFO, info);
    }

    public void notifyConnectingtoInitiator() {
        sendMessage(RedPhone.HANDLE_CONNECTING_TO_INITIATOR, null);
    }

    public void notifyCallDisconnected() {
        if (state == RedPhone.STATE_RINGING)
            handleMissedCall(remoteNumber);

        sendMessage(RedPhone.HANDLE_CALL_DISCONNECTED, null);
        this.terminate();
    }

    public void notifyHandshakeFailed() {
        state = RedPhone.STATE_IDLE;
        outgoingRinger.playFailure();
        sendMessage(RedPhone.HANDLE_HANDSHAKE_FAILED, null);
        this.terminate();
    }

    public void notifyRecipientUnavailable() {
        state = RedPhone.STATE_IDLE;
        outgoingRinger.playFailure();
        sendMessage(RedPhone.HANDLE_RECIPIENT_UNAVAILABLE, null);
        this.terminate();
    }

    public void notifyPerformingHandshake() {
        outgoingRinger.playHandshake();
        sendMessage(RedPhone.HANDLE_PERFORMING_HANDSHAKE, null);
    }

    public void notifyServerFailure() {
        if (state == RedPhone.STATE_RINGING)
            handleMissedCall(remoteNumber);

        state = RedPhone.STATE_IDLE;
        outgoingRinger.playFailure();
        sendMessage(RedPhone.HANDLE_SERVER_FAILURE, null);
        this.terminate();
    }

    public void notifyClientFailure() {
        if (state == RedPhone.STATE_RINGING)
            handleMissedCall(remoteNumber);

        state = RedPhone.STATE_IDLE;
        outgoingRinger.playFailure();
        sendMessage(RedPhone.HANDLE_CLIENT_FAILURE, null);
        this.terminate();
    }

    public void notifyLoginFailed() {
        if (state == RedPhone.STATE_RINGING)
            handleMissedCall(remoteNumber);

        state = RedPhone.STATE_IDLE;
        outgoingRinger.playFailure();
        sendMessage(RedPhone.HANDLE_LOGIN_FAILED, null);
        this.terminate();
    }

    public void notifyNoSuchUser() {
        sendMessage(RedPhone.HANDLE_NO_SUCH_USER, remoteNumber);
        this.terminate();
    }

    public void notifyServerMessage(String message) {
        sendMessage(RedPhone.HANDLE_SERVER_MESSAGE, message);
        this.terminate();
    }

    public void notifyCodecInitFailed(CodecSetupException e) {
        sendMessage(RedPhone.HANDLE_CODEC_INIT_FAILED, e);
        this.terminate();
    }

    public void notifyClientError(String msg) {
        sendMessage(RedPhone.HANDLE_CLIENT_FAILURE, msg);
        this.terminate();
    }

    public void notifyClientError(int messageId) {
        notifyClientError(getString(messageId));
    }

    public void notifyCallConnecting() {
        outgoingRinger.playSonar();
    }

    public void notifyWaitingForResponder() {
    }

    private void sendMessage(int code, Object extra) {
        Message message = Message.obtain();
        message.what = code;
        message.obj = extra;

        if (handler != null)
            handler.sendMessage(message);
        else
            bufferedEvents.add(message);
    }

    private class IntentRunnable implements Runnable {
        private final Intent intent;

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

        public void run() {
            onIntentReceived(intent);
        }
    }

    public class RedPhoneServiceBinder extends Binder {
        public RedPhoneService getService() {
            return RedPhoneService.this;
        }
    }

    @Override
    public boolean isInCall() {
        switch (state) {
        case RedPhone.STATE_IDLE:
            return false;
        case RedPhone.STATE_DIALING:
        case RedPhone.STATE_RINGING:
        case RedPhone.STATE_ANSWERING:
        case RedPhone.STATE_CONNECTED:
            return true;
        default:
            Log.e(TAG, "Unhandled call state: " + state);
            return false;
        }
    }

    private static class ProximityLockRelease implements Thread.UncaughtExceptionHandler {
        private final LockManager lockManager;

        private ProximityLockRelease(LockManager lockManager) {
            this.lockManager = lockManager;
        }

        @Override
        public void uncaughtException(Thread thread, Throwable throwable) {
            Log.d(TAG, "Uncaught exception - releasing proximity lock", throwable);
            lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
        }
    }
}