From fb8440f88654e9dabaa42f75db0d453c49996465 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 5 Feb 2017 12:38:08 -0800 Subject: [PATCH] Correctly manage busy state for (webrtc, redphone, pstn) tuple Fixes #6156 // FREEBIE --- .../redphone/RedPhoneService.java | 93 +++++++++++---- .../redphone/pstn/CallStateView.java | 10 -- .../pstn/IncomingPstnCallListener.java | 51 --------- .../pstn/IncomingPstnCallReceiver.java | 80 +++++++++++++ .../securesms/service/WebRtcCallService.java | 107 +++++++++++++++--- 5 files changed, 241 insertions(+), 100 deletions(-) delete mode 100644 src/org/thoughtcrime/redphone/pstn/CallStateView.java delete mode 100644 src/org/thoughtcrime/redphone/pstn/IncomingPstnCallListener.java create mode 100644 src/org/thoughtcrime/redphone/pstn/IncomingPstnCallReceiver.java diff --git a/src/org/thoughtcrime/redphone/RedPhoneService.java b/src/org/thoughtcrime/redphone/RedPhoneService.java index 57d963ce65..5160682ec8 100644 --- a/src/org/thoughtcrime/redphone/RedPhoneService.java +++ b/src/org/thoughtcrime/redphone/RedPhoneService.java @@ -18,15 +18,20 @@ package org.thoughtcrime.redphone; 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.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.ResultReceiver; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -40,8 +45,7 @@ import org.thoughtcrime.redphone.call.InitiatingCallManager; import org.thoughtcrime.redphone.call.LockManager; import org.thoughtcrime.redphone.call.ResponderCallManager; import org.thoughtcrime.redphone.crypto.zrtp.SASInfo; -import org.thoughtcrime.redphone.pstn.CallStateView; -import org.thoughtcrime.redphone.pstn.IncomingPstnCallListener; +import org.thoughtcrime.redphone.pstn.IncomingPstnCallReceiver; import org.thoughtcrime.redphone.signaling.OtpCounterProvider; import org.thoughtcrime.redphone.signaling.SessionDescriptor; import org.thoughtcrime.redphone.signaling.SignalingException; @@ -56,13 +60,16 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.concurrent.ExecutionException; import de.greenrobot.event.EventBus; @@ -75,7 +82,7 @@ import de.greenrobot.event.EventBus; * @author Moxie Marlinspike * */ -public class RedPhoneService extends Service implements CallStateListener, CallStateView { +public class RedPhoneService extends Service implements CallStateListener { private static final String TAG = RedPhoneService.class.getSimpleName(); @@ -88,6 +95,7 @@ public class RedPhoneService extends Service implements CallStateListener, CallS public static final String EXTRA_REMOTE_NUMBER = "remote_number"; public static final String EXTRA_SESSION_DESCRIPTOR = "session_descriptor"; public static final String EXTRA_MUTE = "mute_value"; + public static final String EXTRA_RESULT_RECEIVER = "result_receiver"; 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"; @@ -95,6 +103,7 @@ public class RedPhoneService extends Service implements CallStateListener, CallS public static final String ACTION_DENY_CALL = "org.thoughtcrime.redphone.RedPhoneService.DENY_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_IS_IN_CALL_QUERY = "org.thoughtcrime.redphone.RedPhoneService.IS_IN_CALL"; private final Handler serviceHandler = new Handler(); @@ -108,7 +117,7 @@ public class RedPhoneService extends Service implements CallStateListener, CallS private LockManager lockManager; private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager; - private IncomingPstnCallListener pstnCallListener; + private IncomingPstnCallReceiver pstnCallListener; @Override public void onCreate() { @@ -150,6 +159,7 @@ public class RedPhoneService extends Service implements CallStateListener, CallS 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_IS_IN_CALL_QUERY)) handleIsInCallQuery(intent); else Log.w(TAG, "Unhandled intent: " + intent.getAction() + ", state: " + state); } @@ -161,7 +171,7 @@ public class RedPhoneService extends Service implements CallStateListener, CallS } private void initializePstnCallListener() { - pstnCallListener = new IncomingPstnCallListener(this); + pstnCallListener = new IncomingPstnCallReceiver(); registerReceiver(pstnCallListener, new IntentFilter("android.intent.action.PHONE_STATE")); } @@ -279,12 +289,22 @@ public class RedPhoneService extends Service implements CallStateListener, CallS } } + private void handleIsInCallQuery(Intent intent) { + ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); + + if (resultReceiver != null) { + resultReceiver.send(state != STATE_IDLE ? 1 : 0, null); + } + } + /// Helper Methods private boolean isBusy() { TelephonyManager telephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); - return ((currentCallManager != null && state != STATE_IDLE) || - telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE); + + return ((currentCallManager != null && state != STATE_IDLE) || + telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) || + WebRtcCallService.isCallActive(this); } private boolean isIdle() { @@ -528,22 +548,6 @@ public class RedPhoneService extends Service implements CallStateListener, CallS } } - @Override - public boolean isInCall() { - switch(state) { - case STATE_IDLE: - return false; - case STATE_DIALING: - case STATE_RINGING: - case STATE_ANSWERING: - case 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; @@ -557,4 +561,47 @@ public class RedPhoneService extends Service implements CallStateListener, CallS lockManager.updatePhoneState(LockManager.PhoneState.IDLE); } } + + @WorkerThread + public static boolean isCallActive(Context context) { + Log.w(TAG, "isCallActive()"); + + HandlerThread handlerThread = null; + + try { + handlerThread = new HandlerThread("webrtc-callback"); + handlerThread.start(); + + final SettableFuture future = new SettableFuture<>(); + + ResultReceiver resultReceiver = new ResultReceiver(new Handler(handlerThread.getLooper())) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.w(TAG, "onReceiveResult"); + future.set(resultCode == 1); + } + }; + + Intent intent = new Intent(context, RedPhoneService.class); + intent.setAction(ACTION_IS_IN_CALL_QUERY); + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver); + + context.startService(intent); + + Log.w(TAG, "Blocking on result..."); + return future.get(); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + return false; + } finally { + if (handlerThread != null) handlerThread.quit(); + } + } + + public static void isCallActive(Context context, ResultReceiver resultReceiver) { + Intent intent = new Intent(context, RedPhoneService.class); + intent.setAction(ACTION_IS_IN_CALL_QUERY); + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver); + + context.startService(intent); + } } diff --git a/src/org/thoughtcrime/redphone/pstn/CallStateView.java b/src/org/thoughtcrime/redphone/pstn/CallStateView.java deleted file mode 100644 index 5a478d93de..0000000000 --- a/src/org/thoughtcrime/redphone/pstn/CallStateView.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.thoughtcrime.redphone.pstn; - -/** - * Provides access to the state of a call - * - * @author Stuart O. Anderson - */ -public interface CallStateView { - boolean isInCall(); -} diff --git a/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallListener.java b/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallListener.java deleted file mode 100644 index c70f6383d7..0000000000 --- a/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallListener.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.thoughtcrime.redphone.pstn; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.telephony.TelephonyManager; -import android.util.Log; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Listens for incoming PSTN calls and rejects them if a RedPhone call is already in progress. - * - * Unstable use of reflection employed to gain access to ITelephony. - * - * @author Stuart O. Anderson - */ -public class IncomingPstnCallListener extends BroadcastReceiver { - private static final String TAG = IncomingPstnCallListener.class.getName(); - private final CallStateView callState; - - public IncomingPstnCallListener(CallStateView callState) { - this.callState = callState; - } - - @Override - public void onReceive(Context context, Intent intent) { - if(!(intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) != null - && intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(TelephonyManager.EXTRA_STATE_RINGING) - && callState.isInCall())) { - return; - } - Log.d(TAG, "Attempting to deny incoming PSTN call."); - TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); - try { - Method getTelephony = tm.getClass().getDeclaredMethod("getITelephony"); - getTelephony.setAccessible(true); - Object telephonyService = getTelephony.invoke(tm); - Method endCall = telephonyService.getClass().getDeclaredMethod("endCall"); - endCall.invoke(telephonyService); - Log.d(TAG, "Denied Incoming Call From: " + intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)); - } catch (NoSuchMethodException e) { - Log.d(TAG, "Unable to access ITelephony API", e); - } catch (IllegalAccessException e) { - Log.d(TAG, "Unable to access ITelephony API", e); - } catch (InvocationTargetException e) { - Log.d(TAG, "Unable to access ITelephony API", e); - } - } -} diff --git a/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallReceiver.java b/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallReceiver.java new file mode 100644 index 0000000000..587605de8c --- /dev/null +++ b/src/org/thoughtcrime/redphone/pstn/IncomingPstnCallReceiver.java @@ -0,0 +1,80 @@ +package org.thoughtcrime.redphone.pstn; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.telephony.TelephonyManager; +import android.util.Log; + +import org.thoughtcrime.redphone.RedPhoneService; +import org.thoughtcrime.securesms.service.WebRtcCallService; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Listens for incoming PSTN calls and rejects them if a RedPhone call is already in progress. + * + * Unstable use of reflection employed to gain access to ITelephony. + * + */ +public class IncomingPstnCallReceiver extends BroadcastReceiver { + + private static final String TAG = IncomingPstnCallReceiver.class.getName(); + + @Override + public void onReceive(Context context, Intent intent) { + Log.w(TAG, "Checking incoming call..."); + + if (intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) == null) { + Log.w(TAG, "Telephony event does not contain number..."); + return; + } + + if (!intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(TelephonyManager.EXTRA_STATE_RINGING)) { + Log.w(TAG, "Telephony event is not state rining..."); + return; + } + + InCallListener listener = new InCallListener(context, new Handler()); + + WebRtcCallService.isCallActive(context, listener); + RedPhoneService.isCallActive(context, listener); + } + + private static class InCallListener extends ResultReceiver { + + private final Context context; + + InCallListener(Context context, Handler handler) { + super(handler); + this.context = context.getApplicationContext(); + } + + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == 1) { + Log.w(TAG, "Attempting to deny incoming PSTN call."); + + TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); + + try { + Method getTelephony = tm.getClass().getDeclaredMethod("getITelephony"); + getTelephony.setAccessible(true); + Object telephonyService = getTelephony.invoke(tm); + Method endCall = telephonyService.getClass().getDeclaredMethod("endCall"); + endCall.invoke(telephonyService); + Log.w(TAG, "Denied Incoming Call."); + } catch (NoSuchMethodException e) { + Log.w(TAG, "Unable to access ITelephony API", e); + } catch (IllegalAccessException e) { + Log.w(TAG, "Unable to access ITelephony API", e); + } catch (InvocationTargetException e) { + Log.w(TAG, "Unable to access ITelephony API", e); + } + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java index 233edc5c5d..c204705e20 100644 --- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -2,13 +2,18 @@ package org.thoughtcrime.securesms.service; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; +import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -16,10 +21,10 @@ import android.util.Pair; import com.google.protobuf.InvalidProtocolBufferException; +import org.thoughtcrime.redphone.RedPhoneService; import org.thoughtcrime.redphone.audio.IncomingRinger; import org.thoughtcrime.redphone.call.LockManager; -import org.thoughtcrime.redphone.pstn.CallStateView; -import org.thoughtcrime.redphone.pstn.IncomingPstnCallListener; +import org.thoughtcrime.redphone.pstn.IncomingPstnCallReceiver; import org.thoughtcrime.redphone.util.AudioUtils; import org.thoughtcrime.redphone.util.UncaughtExceptionHandlerManager; import org.thoughtcrime.securesms.ApplicationContext; @@ -71,6 +76,7 @@ import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; import java.io.IOException; import java.nio.ByteBuffer; @@ -93,7 +99,7 @@ import static org.thoughtcrime.securesms.webrtc.CallNotificationManager.TYPE_EST import static org.thoughtcrime.securesms.webrtc.CallNotificationManager.TYPE_INCOMING_RINGING; import static org.thoughtcrime.securesms.webrtc.CallNotificationManager.TYPE_OUTGOING_RINGING; -public class WebRtcCallService extends Service implements InjectableType, CallStateView, PeerConnection.Observer, DataChannel.Observer { +public class WebRtcCallService extends Service implements InjectableType, PeerConnection.Observer, DataChannel.Observer { private static final String TAG = WebRtcCallService.class.getSimpleName(); @@ -111,6 +117,7 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt public static final String EXTRA_ICE_SDP = "ice_sdp"; public static final String EXTRA_ICE_SDP_MID = "ice_sdp_mid"; public static final String EXTRA_ICE_SDP_LINE_INDEX = "ice_sdp_line_index"; + public static final String EXTRA_RESULT_RECEIVER = "result_receiver"; public static final String ACTION_INCOMING_CALL = "CALL_INCOMING"; public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING"; @@ -120,6 +127,7 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt public static final String ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO"; public static final String ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"; public static final String ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"; + public static final String ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"; public static final String ACTION_RESPONSE_MESSAGE = "RESPONSE_MESSAGE"; public static final String ACTION_ICE_MESSAGE = "ICE_MESSAGE"; @@ -145,6 +153,9 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt private OutgoingRinger outgoingRinger; private LockManager lockManager; + private IncomingPstnCallReceiver callReceiver; + private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager; + @Nullable private Long callId; @Nullable private Recipient recipient; @Nullable private PeerConnectionWrapper peerConnection; @@ -165,7 +176,8 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt initializeResources(); initializeRingers(); - initializePstnCallListener(); + + registerIncomingPstnCallReceiver(); registerUncaughtExceptionHandler(); } @@ -194,12 +206,26 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt else if (intent.getAction().equals(ACTION_ICE_CONNECTED)) handleIceConnected(intent); else if (intent.getAction().equals(ACTION_CALL_CONNECTED)) handleCallConnected(intent); else if (intent.getAction().equals(ACTION_CHECK_TIMEOUT)) handleCheckTimeout(intent); + else if (intent.getAction().equals(ACTION_IS_IN_CALL_QUERY)) handleIsInCallQuery(intent); } }); return START_NOT_STICKY; } + @Override + public void onDestroy() { + super.onDestroy(); + + if (callReceiver != null) { + unregisterReceiver(callReceiver); + } + + if (uncaughtExceptionHandlerManager != null) { + uncaughtExceptionHandlerManager.unregister(); + } + } + // Initializers private void initializeRingers() { @@ -207,11 +233,6 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt this.incomingRinger = new IncomingRinger(this); } - private void initializePstnCallListener() { - IncomingPstnCallListener pstnCallListener = new IncomingPstnCallListener(this); - registerReceiver(pstnCallListener, new IntentFilter("android.intent.action.PHONE_STATE")); - } - private void initializeResources() { ApplicationContext.getInstance(this).injectDependencies(this); @@ -223,8 +244,13 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt this.accountManager.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); } + private void registerIncomingPstnCallReceiver() { + callReceiver = new IncomingPstnCallReceiver(); + registerReceiver(callReceiver, new IntentFilter("android.intent.action.PHONE_STATE")); + } + private void registerUncaughtExceptionHandler() { - UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager = new UncaughtExceptionHandlerManager(); + uncaughtExceptionHandlerManager = new UncaughtExceptionHandlerManager(); uncaughtExceptionHandlerManager.registerHandler(new ProximityLockRelease(lockManager)); } @@ -515,6 +541,14 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt } } + private void handleIsInCallQuery(Intent intent) { + ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER); + + if (resultReceiver != null) { + resultReceiver.send(callState != CallState.STATE_IDLE ? 1 : 0, null); + } + } + private void insertMissedCall(@NonNull Recipient recipient, boolean signal) { Pair messageAndThreadId = DatabaseFactory.getSmsDatabase(this).insertMissedCall(recipient.getNumber()); MessageNotifier.updateNotification(this, KeyCachingService.getMasterSecret(this), @@ -643,7 +677,10 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt private boolean isBusy() { TelephonyManager telephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); - return callState != CallState.STATE_IDLE || telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE; + + return callState != CallState.STATE_IDLE || + telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE || + RedPhoneService.isCallActive(this); } private boolean isIdle() { @@ -793,11 +830,6 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt return null; } - @Override - public boolean isInCall() { - return false; - } - /// PeerConnection Observer @Override public void onSignalingChange(PeerConnection.SignalingState newState) { @@ -1067,4 +1099,47 @@ public class WebRtcCallService extends Service implements InjectableType, CallSt throw new AssertionError(throwable); } } + + @WorkerThread + public static boolean isCallActive(Context context) { + Log.w(TAG, "isCallActive()"); + + HandlerThread handlerThread = null; + + try { + handlerThread = new HandlerThread("webrtc-callback"); + handlerThread.start(); + + final SettableFuture future = new SettableFuture<>(); + + ResultReceiver resultReceiver = new ResultReceiver(new Handler(handlerThread.getLooper())) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.w(TAG, "Got result..."); + future.set(resultCode == 1); + } + }; + + Intent intent = new Intent(context, WebRtcCallService.class); + intent.setAction(ACTION_IS_IN_CALL_QUERY); + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver); + + context.startService(intent); + + Log.w(TAG, "Blocking on result..."); + return future.get(); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + return false; + } finally { + if (handlerThread != null) handlerThread.quit(); + } + } + + public static void isCallActive(Context context, ResultReceiver resultReceiver) { + Intent intent = new Intent(context, WebRtcCallService.class); + intent.setAction(ACTION_IS_IN_CALL_QUERY); + intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver); + + context.startService(intent); + } }