Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
genofire 2021-11-25 21:20:44 +01:00
commit 658c1c58d5
23 changed files with 902 additions and 479 deletions

View File

@ -76,7 +76,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:4.9.2" implementation "com.squareup.okhttp3:okhttp:4.9.2"
implementation 'com.google.guava:guava:30.1.1-android' implementation 'com.google.guava:guava:30.1.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36'
// implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') // implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.32006' implementation 'org.webrtc:google-webrtc:1.0.32006'
} }

View File

@ -488,15 +488,24 @@ public class NotificationService {
notify(INCOMING_CALL_NOTIFICATION_ID, notification); notify(INCOMING_CALL_NOTIFICATION_ID, notification);
} }
public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) { public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) {
final AbstractJingleConnection.Id id = ongoingCall.id;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls");
if (media.contains(Media.VIDEO)) { if (ongoingCall.media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
}
} else { } else {
builder.setSmallIcon(R.drawable.ic_call_white_24dp); builder.setSmallIcon(R.drawable.ic_call_white_24dp);
if (ongoingCall.reconnecting) {
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call));
} else {
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
} }
}
builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setPriority(NotificationCompat.PRIORITY_HIGH);

View File

@ -572,8 +572,8 @@ public class XmppConnectionService extends Service {
} }
} }
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri); final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
final String compressPictures = getCompressPicturesPreference(); final String compressPictures = getCompressPicturesPreference();
if ("never".equals(compressPictures) if ("never".equals(compressPictures)
@ -1298,8 +1298,8 @@ public class XmppConnectionService extends Service {
toggleForegroundService(false); toggleForegroundService(false);
} }
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) { public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
ongoingCall.set(new OngoingCall(id, media)); ongoingCall.set(new OngoingCall(id, media, reconnecting));
toggleForegroundService(false); toggleForegroundService(false);
} }
@ -1315,7 +1315,7 @@ public class XmppConnectionService extends Service {
final Notification notification; final Notification notification;
final int id; final int id;
if (ongoing != null) { if (ongoing != null) {
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media); notification = this.mNotificationService.getOngoingCallNotification(ongoing);
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
startForeground(id, notification); startForeground(id, notification);
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
@ -4869,12 +4869,14 @@ public class XmppConnectionService extends Service {
} }
public static class OngoingCall { public static class OngoingCall {
private final AbstractJingleConnection.Id id; public final AbstractJingleConnection.Id id;
private final Set<Media> media; public final Set<Media> media;
public final boolean reconnecting;
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) { public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
this.id = id; this.id = id;
this.media = media; this.media = media;
this.reconnecting = reconnecting;
} }
@Override @Override
@ -4882,12 +4884,12 @@ public class XmppConnectionService extends Service {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
OngoingCall that = (OngoingCall) o; OngoingCall that = (OngoingCall) o;
return Objects.equal(id, that.id); return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(id); return Objects.hashCode(id, media, reconnecting);
} }
} }
} }

View File

@ -688,14 +688,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
toggleInputMethod(); toggleInputMethod();
} }
private void attachImageToConversation(Conversation conversation, Uri uri) { private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
if (conversation == null) { if (conversation == null) {
return; return;
} }
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
prepareFileToast.show(); prepareFileToast.show();
activity.delegateUriPermissionsToService(uri); activity.delegateUriPermissionsToService(uri);
activity.xmppConnectionService.attachImageToConversation(conversation, uri, activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
new UiCallback<Message>() { new UiCallback<Message>() {
@Override @Override
@ -856,9 +856,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
toggleInputMethod(); toggleInputMethod();
break; break;
case ATTACHMENT_CHOICE_LOCATION: case ATTACHMENT_CHOICE_LOCATION:
double latitude = data.getDoubleExtra("latitude", 0); final double latitude = data.getDoubleExtra("latitude", 0);
double longitude = data.getDoubleExtra("longitude", 0); final double longitude = data.getDoubleExtra("longitude", 0);
Uri geo = Uri.parse("geo:" + latitude + "," + longitude); final int accuracy = data.getIntExtra("accuracy", 0);
final Uri geo;
if (accuracy > 0) {
geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy));
} else {
geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
}
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
toggleInputMethod(); toggleInputMethod();
break; break;
@ -889,7 +895,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
attachLocationToConversation(conversation, attachment.getUri()); attachLocationToConversation(conversation, attachment.getUri());
} else if (attachment.getType() == Attachment.Type.IMAGE) { } else if (attachment.getType() == Attachment.Type.IMAGE) {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
attachImageToConversation(conversation, attachment.getUri()); attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
} else { } else {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); attachFileToConversation(conversation, attachment.getUri(), attachment.getMime());
@ -2185,13 +2191,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
final String type = extras.getString(ConversationsActivity.EXTRA_TYPE);
final List<Uri> uris = extractUris(extras); final List<Uri> uris = extractUris(extras);
if (uris != null && uris.size() > 0) { if (uris != null && uris.size() > 0) {
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
} else { } else {
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris)); final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris)); mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
} }
toggleInputMethod(); toggleInputMethod();
return; return;

View File

@ -99,6 +99,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
public static final String EXTRA_DO_NOT_APPEND = "do_not_append"; public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
public static final String EXTRA_POST_INIT_ACTION = "post_init_action"; public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
public static final String POST_ACTION_RECORD_VOICE = "record_voice"; public static final String POST_ACTION_RECORD_VOICE = "record_voice";
public static final String EXTRA_TYPE = "type";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList( private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION, ACTION_VIEW_CONVERSATION,

View File

@ -96,7 +96,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
); );
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
RtpEndUserState.CONNECTING, RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED = Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING
);
private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING,
RtpEndUserState.RECONNECTING
); );
private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
private static final int REQUEST_ACCEPT_CALL = 0x1111; private static final int REQUEST_ACCEPT_CALL = 0x1111;
@ -502,7 +512,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private boolean isConnected() { private boolean isConnected() {
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED; return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
} }
private boolean switchToPictureInPicture() { private boolean switchToPictureInPicture() {
@ -635,8 +645,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
surfaceViewRenderer.setVisibility(View.VISIBLE); surfaceViewRenderer.setVisibility(View.VISIBLE);
try { try {
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
} catch (IllegalStateException e) { } catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
} }
surfaceViewRenderer.setEnableHardwareScaler(true); surfaceViewRenderer.setEnableHardwareScaler(true);
} }
@ -661,6 +671,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case CONNECTED: case CONNECTED:
setTitle(R.string.rtp_state_connected); setTitle(R.string.rtp_state_connected);
break; break;
case RECONNECTING:
setTitle(R.string.rtp_state_reconnecting);
break;
case ACCEPTING_CALL: case ACCEPTING_CALL:
setTitle(R.string.rtp_state_accepting_call); setTitle(R.string.rtp_state_accepting_call);
break; break;
@ -803,7 +816,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) { private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) { if (media.contains(Media.VIDEO)) {
final JingleRtpConnection rtpConnection = requireRtpConnection(); final JingleRtpConnection rtpConnection = requireRtpConnection();
@ -931,14 +944,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.duration.setVisibility(View.GONE); this.binding.duration.setVisibility(View.GONE);
return; return;
} }
final long rtpConnectionStarted = connection.getRtpConnectionStarted(); if (connection.zeroDuration()) {
final long rtpConnectionEnded = connection.getRtpConnectionEnded();
if (rtpConnectionStarted != 0) {
final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
this.binding.duration.setVisibility(View.VISIBLE);
} else {
this.binding.duration.setVisibility(View.GONE); this.binding.duration.setVisibility(View.GONE);
} else {
this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
this.binding.duration.setVisibility(View.VISIBLE);
} }
} }
@ -970,7 +980,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
return; return;
} }
if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) { if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
binding.localVideo.setVisibility(View.GONE); binding.localVideo.setVisibility(View.GONE);
binding.remoteVideoWrapper.setVisibility(View.GONE); binding.remoteVideoWrapper.setVisibility(View.GONE);
binding.appBarLayout.setVisibility(View.GONE); binding.appBarLayout.setVisibility(View.GONE);
@ -1003,6 +1013,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideoWrapper.setVisibility(View.VISIBLE); binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
} else { } else {
binding.appBarLayout.setVisibility(View.VISIBLE);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
binding.remoteVideoWrapper.setVisibility(View.GONE); binding.remoteVideoWrapper.setVisibility(View.GONE);
} }

View File

@ -13,10 +13,13 @@ import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.google.common.math.DoubleMath;
import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import java.math.RoundingMode;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityShareLocationBinding; import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
@ -54,7 +57,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location);
setSupportActionBar(binding.toolbar); setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar()); configureActionBar(getSupportActionBar());
setupMapView(binding.map, LocationProvider.getGeoPoint(this)); setupMapView(binding.map, LocationProvider.getGeoPoint(this));
@ -76,23 +79,7 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
}); });
ThemeHelper.fix(this.snackBar); ThemeHelper.fix(this.snackBar);
this.binding.shareButton.setOnClickListener(view -> { this.binding.shareButton.setOnClickListener(this::shareLocation);
final Intent result = new Intent();
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", (int) myLoc.getAccuracy());
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
setResult(RESULT_OK, result);
finish();
});
this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
@ -108,6 +95,22 @@ public class ShareLocationActivity extends LocationActivity implements LocationL
}); });
} }
private void shareLocation(final View view) {
final Intent result = new Intent();
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP));
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
setResult(RESULT_OK, result);
finish();
}
@Override @Override
public void onRequestPermissionsResult(final int requestCode, public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions, @NonNull final String[] permissions,

View File

@ -33,7 +33,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
refreshUi(); refreshUi();
} }
private class Share { private static class Share {
public String type;
ArrayList<Uri> uris = new ArrayList<>(); ArrayList<Uri> uris = new ArrayList<>();
public String account; public String account;
public String contact; public String contact;
@ -65,6 +66,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0) if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == REQUEST_STORAGE_PERMISSION) { if (requestCode == REQUEST_STORAGE_PERMISSION) {
@ -139,6 +141,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else if (type != null && uri != null) { } else if (type != null && uri != null) {
this.share.uris.clear(); this.share.uris.clear();
this.share.uris.add(uri); this.share.uris.add(uri);
this.share.type = type;
} else { } else {
this.share.text = text; this.share.text = text;
this.share.asQuote = asQuote; this.share.asQuote = asQuote;
@ -193,6 +196,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.setAction(Intent.ACTION_SEND_MULTIPLE);
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (share.type != null) {
intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type);
}
} else if (share.text != null) { } else if (share.text != null) {
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
intent.putExtra(Intent.EXTRA_TEXT, share.text); intent.putExtra(Intent.EXTRA_TEXT, share.text);

View File

@ -136,10 +136,10 @@ public class Attachment implements Parcelable {
return Collections.singletonList(new Attachment(uri, type, mime)); return Collections.singletonList(new Attachment(uri, type, mime));
} }
public static List<Attachment> of(final Context context, List<Uri> uris) { public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
List<Attachment> attachments = new ArrayList<>(); final List<Attachment> attachments = new ArrayList<>();
for (Uri uri : uris) { for (final Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
} }
return attachments; return attachments;

View File

@ -4,6 +4,8 @@ import android.content.Context;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import androidx.core.content.ContextCompat;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -16,11 +18,14 @@ import eu.siacs.conversations.R;
public class LocationProvider { public class LocationProvider {
public static final GeoPoint FALLBACK = new GeoPoint(0.0,0.0); public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0);
public static String getUserCountry(Context context) { public static String getUserCountry(final Context context) {
try { try {
final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class);
if (tm == null) {
return getUserCountryFallback();
}
final String simCountry = tm.getSimCountryIso(); final String simCountry = tm.getSimCountryIso();
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
return simCountry.toUpperCase(Locale.US); return simCountry.toUpperCase(Locale.US);
@ -30,38 +35,39 @@ public class LocationProvider {
return networkCountry.toUpperCase(Locale.US); return networkCountry.toUpperCase(Locale.US);
} }
} }
} catch (Exception e) { return getUserCountryFallback();
// fallthrough } catch (final Exception e) {
return getUserCountryFallback();
} }
Locale locale = Locale.getDefault(); }
private static String getUserCountryFallback() {
final Locale locale = Locale.getDefault();
return locale.getCountry(); return locale.getCountry();
} }
public static GeoPoint getGeoPoint(Context context) { public static GeoPoint getGeoPoint(final Context context) {
return getGeoPoint(context, getUserCountry(context)); return getGeoPoint(context, getUserCountry(context));
} }
public static synchronized GeoPoint getGeoPoint(Context context, String country) { public static synchronized GeoPoint getGeoPoint(final Context context, final String country) {
try { try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) {
BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)));
String line; String line;
while((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
String[] parts = line.split("\\s+",4); final String[] parts = line.split("\\s+", 4);
if (parts.length == 4) { if (parts.length == 4) {
if (country.equalsIgnoreCase(parts[0])) { if (country.equalsIgnoreCase(parts[0])) {
try { try {
return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2])); return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
} catch (NumberFormatException e) { } catch (final NumberFormatException e) {
return FALLBACK; return FALLBACK;
} }
} }
} else {
Log.d(Config.LOGTAG,"unable to parse line="+line);
} }
} }
} catch (IOException e) { } catch (final IOException e) {
Log.d(Config.LOGTAG,e.getMessage()); Log.d(Config.LOGTAG, "unable to parse country->geo map", e);
} }
return FALLBACK; return FALLBACK;
} }

View File

@ -71,10 +71,14 @@ public class TimeFrameUtils {
public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) { public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) {
final long passed = (since < 0) ? 0 : (to - since); final long passed = (since < 0) ? 0 : (to - since);
final int hours = (int) (passed / 3600000); return formatElapsedTime(passed, withMilliseconds);
final int minutes = (int) (passed / 60000) % 60; }
final int seconds = (int) (passed / 1000) % 60;
final int milliseconds = (int) (passed / 100) % 10; public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) {
final int hours = (int) (elapsed / 3600000);
final int minutes = (int) (elapsed / 60000) % 60;
final int seconds = (int) (elapsed / 1000) % 60;
final int milliseconds = (int) (elapsed / 100) % 10;
if (hours > 0) { if (hours > 0) {
return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds); return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds);
} else if (withMilliseconds) { } else if (withMilliseconds) {

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.xml; package eu.siacs.conversations.xml;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
@ -165,8 +167,9 @@ public class Element {
return this.attributes; return this.attributes;
} }
@NotNull
public String toString() { public String toString() {
StringBuilder elementOutput = new StringBuilder(); final StringBuilder elementOutput = new StringBuilder();
if ((content == null) && (children.size() == 0)) { if ((content == null) && (children.size() == 0)) {
Tag emptyTag = Tag.empty(name); Tag emptyTag = Tag.empty(name);
emptyTag.setAtttributes(this.attributes); emptyTag.setAtttributes(this.attributes);

View File

@ -28,6 +28,7 @@ public final class Namespace {
public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE = "urn:xmpp:jingle:1";
public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1";
public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"; public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";

View File

@ -54,7 +54,6 @@ import javax.net.ssl.X509TrustManager;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.sasl.Anonymous; import eu.siacs.conversations.crypto.sasl.Anonymous;

View File

@ -206,7 +206,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final Element error = response.addChild("error"); final Element error = response.addChild("error");
error.setAttribute("type", conditionType); error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1"); error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
account.getXmppConnection().sendIqPacket(response, null); account.getXmppConnection().sendIqPacket(response, null);
} }

View File

@ -1,6 +1,5 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -8,6 +7,7 @@ import androidx.annotation.Nullable;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
@ -25,13 +25,15 @@ import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
import java.util.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -139,7 +141,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>(); private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>> pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification(); private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message; private final Message message;
private State state = State.NULL; private State state = State.NULL;
@ -147,8 +149,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private Set<Media> proposedMedia; private Set<Media> proposedMedia;
private RtpContentMap initiatorRtpContentMap; private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap; private RtpContentMap responderRtpContentMap;
private long rtpConnectionStarted = 0; //time of 'connected' private IceUdpTransportInfo.Setup peerDtlsSetup;
private long rtpConnectionEnded = 0; private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
private ScheduledFuture<?> ringingTimeoutFuture; private ScheduledFuture<?> ringingTimeoutFuture;
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
@ -190,7 +193,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
synchronized void deliverPacket(final JinglePacket jinglePacket) { synchronized void deliverPacket(final JinglePacket jinglePacket) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
switch (jinglePacket.getAction()) { switch (jinglePacket.getAction()) {
case SESSION_INITIATE: case SESSION_INITIATE:
receiveSessionInitiate(jinglePacket); receiveSessionInitiate(jinglePacket);
@ -251,24 +253,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveTransportInfo(final JinglePacket jinglePacket) { private void receiveTransportInfo(final JinglePacket jinglePacket) {
//Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received
if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
final RtpContentMap contentMap; final RtpContentMap contentMap;
try { try {
contentMap = RtpContentMap.of(jinglePacket); contentMap = RtpContentMap.of(jinglePacket);
} catch (IllegalArgumentException | NullPointerException e) { } catch (final IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
respondOk(jinglePacket);
return; return;
} }
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet(); receiveTransportInfo(jinglePacket, contentMap);
if (this.state == State.SESSION_ACCEPTED) {
try {
processCandidates(candidates);
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored");
}
} else {
pendingIceCandidates.push(candidates);
}
} else { } else {
if (isTerminated()) { if (isTerminated()) {
respondOk(jinglePacket); respondOk(jinglePacket);
@ -280,28 +273,140 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} }
private void processCandidates(final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) { private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) {
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
final Group originalGroup = rtpContentMap.group; if (this.state == State.SESSION_ACCEPTED) {
final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); //zero candidates + modified credentials are an ICE restart offer
if (identificationTags.size() == 0) { if (checkForIceRestart(jinglePacket, contentMap)) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); return;
}
respondOk(jinglePacket);
try {
processCandidates(candidates);
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored");
}
} else {
respondOk(jinglePacket);
pendingIceCandidates.addAll(candidates);
} }
processCandidates(identificationTags, contents);
} }
private void processCandidates(final List<String> indices, final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) { private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
final RtpContentMap existing = getRemoteContentMap();
final IceUdpTransportInfo.Credentials existingCredentials;
final IceUdpTransportInfo.Credentials newCredentials;
try {
existingCredentials = existing.getCredentials();
newCredentials = rtpContentMap.getCredentials();
} catch (final IllegalStateException e) {
Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e);
return false;
}
if (existingCredentials.equals(newCredentials)) {
return false;
}
//TODO an alternative approach is to check if we already got an iq result to our ICE-restart
// and if that's the case we are seeing an answer.
// This might be more spec compliant but also more error prone potentially
final boolean isOffer = rtpContentMap.emptyCandidates();
final RtpContentMap restartContentMap;
try {
if (isOffer) {
Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials);
restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS);
} else {
final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup();
Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup);
// DTLS setup attribute needs to be rewritten to reflect current peer state
// https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM
restartContentMap = existing.modifiedCredentials(newCredentials, setup);
}
if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) {
return isOffer;
} else {
Log.d(Config.LOGTAG, "ignoring ICE restart. sending tie-break");
respondWithTieBreak(jinglePacket);
return true;
}
} catch (final Exception exception) {
respondOk(jinglePacket);
final Throwable rootCause = Throwables.getRootCause(exception);
if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) {
//If this happens a termination is already in progress
Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart");
return true;
}
Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause);
webRTCWrapper.close();
sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
return true;
}
}
private IceUdpTransportInfo.Setup getPeerDtlsSetup() {
final IceUdpTransportInfo.Setup peerSetup = this.peerDtlsSetup;
if (peerSetup == null || peerSetup == IceUdpTransportInfo.Setup.ACTPASS) {
throw new IllegalStateException("Invalid peer setup");
}
return peerSetup;
}
private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) {
if (setup == null || setup == IceUdpTransportInfo.Setup.ACTPASS) {
throw new IllegalArgumentException("Trying to store invalid peer dtls setup");
}
this.peerDtlsSetup = setup;
}
private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription = SessionDescription.of(restartContentMap);
final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER;
org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString());
if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) {
if (isInitiator()) {
//We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map
return false;
}
}
webRTCWrapper.setRemoteDescription(sdp).get();
setRemoteContentMap(restartContentMap);
if (isOffer) {
webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
final SessionDescription localSessionDescription = setLocalSessionDescription();
setLocalContentMap(RtpContentMap.of(localSessionDescription));
//We need to respond OK before sending any candidates
respondOk(jinglePacket);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
} else {
storePeerDtlsSetup(restartContentMap.getDtlsSetup());
}
return true;
}
private void processCandidates(final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) { for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
final String ufrag = content.getValue().transport.getAttribute("ufrag"); processCandidate(content);
for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { }
}
private void processCandidate(final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
final RtpContentMap rtpContentMap = getRemoteContentMap();
final List<String> indices = toIdentificationTags(rtpContentMap);
final String sdpMid = content.getKey(); //aka content name
final IceUdpTransportInfo transport = content.getValue().transport;
final IceUdpTransportInfo.Credentials credentials = transport.getCredentials();
//TODO check that credentials remained the same
for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) {
final String sdp; final String sdp;
try { try {
sdp = candidate.toSdpAttribute(ufrag); sdp = candidate.toSdpAttribute(credentials.ufrag);
} catch (IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage());
continue; continue;
} }
final String sdpMid = content.getKey();
final int mLineIndex = indices.indexOf(sdpMid); final int mLineIndex = indices.indexOf(sdpMid);
if (mLineIndex < 0) { if (mLineIndex < 0) {
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
@ -311,6 +416,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.webRTCWrapper.addIceCandidate(iceCandidate); this.webRTCWrapper.addIceCandidate(iceCandidate);
} }
} }
private RtpContentMap getRemoteContentMap() {
return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
}
private List<String> toIdentificationTags(final RtpContentMap rtpContentMap) {
final Group originalGroup = rtpContentMap.group;
final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags();
if (identificationTags.size() == 0) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
}
return identificationTags;
} }
private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
@ -370,7 +487,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) {
try { try {
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(); contentMap.requireDTLSFingerprint(true);
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e));
respondOk(jinglePacket); respondOk(jinglePacket);
@ -398,11 +515,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket); respondOk(jinglePacket);
pendingIceCandidates.addAll(contentMap.contents.entrySet());
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
if (candidates.size() > 0) {
pendingIceCandidates.push(candidates);
}
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
sendSessionAccept(); sendSessionAccept();
@ -471,6 +584,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveSessionAccept(final RtpContentMap contentMap) { private void receiveSessionAccept(final RtpContentMap contentMap) {
this.responderRtpContentMap = contentMap; this.responderRtpContentMap = contentMap;
this.storePeerDtlsSetup(contentMap.getDtlsSetup());
final SessionDescription sessionDescription; final SessionDescription sessionDescription;
try { try {
sessionDescription = SessionDescription.of(contentMap); sessionDescription = SessionDescription.of(contentMap);
@ -489,11 +603,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} catch (final Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e));
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION); sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage());
return; return;
} }
final List<String> identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags(); processCandidates(contentMap.contents.entrySet());
processCandidates(identificationTags, contentMap.contents.entrySet());
} }
private void sendSessionAccept() { private void sendSessionAccept() {
@ -537,7 +650,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try { try {
this.webRTCWrapper.setRemoteDescription(sdp).get(); this.webRTCWrapper.setRemoteDescription(sdp).get();
addIceCandidatesFromBlackLog(); addIceCandidatesFromBlackLog();
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get();
prepareSessionAccept(webRTCSessionDescription); prepareSessionAccept(webRTCSessionDescription);
} catch (final Exception e) { } catch (final Exception e) {
failureToAcceptSession(e); failureToAcceptSession(e);
@ -548,15 +661,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (isTerminated()) { if (isTerminated()) {
return; return;
} }
Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(throwable)); final Throwable rootCause = Throwables.getRootCause(throwable);
Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.ofThrowable(throwable)); sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
} }
private void addIceCandidatesFromBlackLog() { private void addIceCandidatesFromBlackLog() {
while (!this.pendingIceCandidates.isEmpty()) { Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
processCandidates(this.pendingIceCandidates.poll()); while ((foo = this.pendingIceCandidates.poll()) != null) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log"); processCandidate(foo);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log");
} }
} }
@ -564,12 +679,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
this.responderRtpContentMap = respondingRtpContentMap; this.responderRtpContentMap = respondingRtpContentMap;
storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap);
Futures.addCallback(outgoingContentMapFuture, Futures.addCallback(outgoingContentMapFuture,
new FutureCallback<RtpContentMap>() { new FutureCallback<RtpContentMap>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionAccept(outgoingContentMap, webRTCSessionDescription); sendSessionAccept(outgoingContentMap);
} }
@Override @Override
@ -581,7 +698,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
); );
} }
private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription) { private void sendSessionAccept(final RtpContentMap rtpContentMap) {
if (isTerminated()) { if (isTerminated()) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do.");
return; return;
@ -589,11 +706,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
transitionOrThrow(State.SESSION_ACCEPTED); transitionOrThrow(State.SESSION_ACCEPTED);
final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
send(sessionAccept); send(sessionAccept);
try {
webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) {
failureToAcceptSession(e);
}
} }
private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) {
@ -841,9 +953,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return; return;
} }
try { try {
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get();
prepareSessionInitiate(webRTCSessionDescription, targetState); prepareSessionInitiate(webRTCSessionDescription, targetState);
} catch (final Exception e) { } catch (final Exception e) {
//TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions
failureToInitiateSession(e, targetState); failureToInitiateSession(e, targetState);
} }
} }
@ -873,11 +986,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
this.initiatorRtpContentMap = rtpContentMap; this.initiatorRtpContentMap = rtpContentMap;
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap);
Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() { Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState); sendSessionInitiate(outgoingContentMap, targetState);
} }
@Override @Override
@ -887,7 +1001,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
if (isTerminated()) { if (isTerminated()) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do.");
return; return;
@ -895,11 +1009,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.transitionOrThrow(targetState); this.transitionOrThrow(targetState);
final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
send(sessionInitiate); send(sessionInitiate);
try {
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) {
failureToInitiateSession(e, targetState);
}
} }
private ListenableFuture<RtpContentMap> encryptSessionInitiate(final RtpContentMap rtpContentMap) { private ListenableFuture<RtpContentMap> encryptSessionInitiate(final RtpContentMap rtpContentMap) {
@ -965,6 +1074,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private synchronized void handleIqResponse(final Account account, final IqPacket response) { private synchronized void handleIqResponse(final Account account, final IqPacket response) {
if (response.getType() == IqPacket.TYPE.ERROR) { if (response.getType() == IqPacket.TYPE.ERROR) {
handleIqErrorResponse(response);
return;
}
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
handleIqTimeoutResponse(response);
}
}
private void handleIqErrorResponse(final IqPacket response) {
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
final String errorCondition = response.getErrorCondition(); final String errorCondition = response.getErrorCondition();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition);
if (isTerminated()) { if (isTerminated()) {
@ -985,7 +1104,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
transitionOrThrow(target); transitionOrThrow(target);
this.finish(); this.finish();
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) { }
private void handleIqTimeoutResponse(final IqPacket response) {
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error");
if (isTerminated()) { if (isTerminated()) {
Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated");
@ -995,7 +1117,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
this.finish(); this.finish();
} }
}
private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order");
@ -1005,8 +1126,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.finish(); this.finish();
} }
private void respondWithTieBreak(final JinglePacket jinglePacket) {
respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
}
private void respondWithOutOfOrder(final JinglePacket jinglePacket) { private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
}
void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) {
jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType);
} }
private void respondOk(final JinglePacket jinglePacket) { private void respondOk(final JinglePacket jinglePacket) {
@ -1043,24 +1172,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.CONNECTING; return RtpEndUserState.CONNECTING;
} }
case SESSION_ACCEPTED: case SESSION_ACCEPTED:
//TODO refactor this out into separate method (that uses switch for better readability) return getPeerConnectionStateAsEndUserState();
final PeerConnection.PeerConnectionState state;
try {
state = webRTCWrapper.getState();
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
//We usually close the WebRTCWrapper *before* transitioning so we might still
//be in SESSION_ACCEPTED even though the peerConnection has been torn down
return RtpEndUserState.ENDING_CALL;
}
if (state == PeerConnection.PeerConnectionState.CONNECTED) {
return RtpEndUserState.CONNECTED;
} else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) {
return RtpEndUserState.CONNECTING;
} else if (state == PeerConnection.PeerConnectionState.CLOSED) {
return RtpEndUserState.ENDING_CALL;
} else {
return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
}
case REJECTED: case REJECTED:
case REJECTED_RACED: case REJECTED_RACED:
case TERMINATED_DECLINED_OR_BUSY: case TERMINATED_DECLINED_OR_BUSY:
@ -1081,7 +1193,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.RETRACTED; return RtpEndUserState.RETRACTED;
} }
case TERMINATED_CONNECTIVITY_ERROR: case TERMINATED_CONNECTIVITY_ERROR:
return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
case TERMINATED_APPLICATION_FAILURE: case TERMINATED_APPLICATION_FAILURE:
return RtpEndUserState.APPLICATION_ERROR; return RtpEndUserState.APPLICATION_ERROR;
case TERMINATED_SECURITY_ERROR: case TERMINATED_SECURITY_ERROR:
@ -1090,6 +1202,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
} }
private RtpEndUserState getPeerConnectionStateAsEndUserState() {
final PeerConnection.PeerConnectionState state;
try {
state = webRTCWrapper.getState();
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
//We usually close the WebRTCWrapper *before* transitioning so we might still
//be in SESSION_ACCEPTED even though the peerConnection has been torn down
return RtpEndUserState.ENDING_CALL;
}
switch (state) {
case CONNECTED:
return RtpEndUserState.CONNECTED;
case NEW:
case CONNECTING:
return RtpEndUserState.CONNECTING;
case CLOSED:
return RtpEndUserState.ENDING_CALL;
default:
return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING;
}
}
public Set<Media> getMedia() { public Set<Media> getMedia() {
final State current = getState(); final State current = getState();
if (current == State.NULL) { if (current == State.NULL) {
@ -1332,7 +1467,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
public void onIceCandidate(final IceCandidate iceCandidate) { public void onIceCandidate(final IceCandidate iceCandidate) {
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
final String ufrag = rtpContentMap.getCredentials().ufrag;
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag);
if (candidate == null) {
Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString());
return;
}
Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString());
sendTransportInfo(iceCandidate.sdpMid, candidate); sendTransportInfo(iceCandidate.sdpMid, candidate);
} }
@ -1340,28 +1481,95 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.stateHistory.add(newState);
this.rtpConnectionStarted = SystemClock.elapsedRealtime(); if (newState == PeerConnection.PeerConnectionState.CONNECTED) {
this.sessionDuration.start();
updateOngoingCallNotification();
} else if (this.sessionDuration.isRunning()) {
this.sessionDuration.stop();
updateOngoingCallNotification();
} }
if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) {
this.rtpConnectionEnded = SystemClock.elapsedRealtime(); final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED);
}
//TODO 'failed' means we need to restart ICE if (newState == PeerConnection.PeerConnectionState.FAILED) {
// if (neverConnected) {
//TODO 'disconnected' can probably be ignored as "This is a less stringent test than failed
// and may trigger intermittently and resolve just as spontaneously on less reliable networks,
// or during temporary disconnections. When the problem resolves, the connection may return
// to the connected state."
// Obviously the UI needs to reflect this new state with a 'reconnecting' display or something
if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) {
if (isTerminated()) { if (isTerminated()) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state);
return; return;
} }
new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
return;
} else { } else {
webRTCWrapper.restartIce();
}
}
updateEndUserState(); updateEndUserState();
} }
@Override
public void onRenegotiationNeeded() {
this.webRTCWrapper.execute(this::initiateIceRestart);
}
private void initiateIceRestart() {
//TODO discover new TURN/STUN credentials
this.stateHistory.clear();
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
final SessionDescription sessionDescription;
try {
sessionDescription = setLocalSessionDescription();
} catch (final Exception e) {
final Throwable cause = Throwables.getRootCause(e);
Log.d(Config.LOGTAG, "failed to renegotiate", cause);
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return;
}
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
final RtpContentMap transportInfo = rtpContentMap.transportInfo();
final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
jinglePacket.setTo(id.with);
xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, "received success to our ice restart");
setLocalContentMap(rtpContentMap);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
return;
}
if (response.getType() == IqPacket.TYPE.ERROR) {
final Element error = response.findChild("error");
if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
return;
}
handleIqErrorResponse(response);
}
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
handleIqTimeoutResponse(response);
}
});
}
private void setLocalContentMap(final RtpContentMap rtpContentMap) {
if (isInitiator()) {
this.initiatorRtpContentMap = rtpContentMap;
} else {
this.responderRtpContentMap = rtpContentMap;
}
}
private void setRemoteContentMap(final RtpContentMap rtpContentMap) {
if (isInitiator()) {
this.responderRtpContentMap = rtpContentMap;
} else {
this.initiatorRtpContentMap = rtpContentMap;
}
}
private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException {
final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get();
return SessionDescription.parse(sessionDescription.description);
} }
private void closeWebRTCSessionAfterFailedConnection() { private void closeWebRTCSessionAfterFailedConnection() {
@ -1375,12 +1583,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} }
public long getRtpConnectionStarted() { public boolean zeroDuration() {
return this.rtpConnectionStarted; return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0;
} }
public long getRtpConnectionEnded() { public long getCallDuration() {
return this.rtpConnectionEnded; return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
} }
public AppRTCAudioManager getAudioManager() { public AppRTCAudioManager getAudioManager() {
@ -1427,8 +1635,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private void updateOngoingCallNotification() { private void updateOngoingCallNotification() {
if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { final State state = this.state;
xmppConnectionService.setOngoingCall(id, getMedia()); if (STATES_SHOWING_ONGOING_CALL.contains(state)) {
final boolean reconnecting;
if (state == State.SESSION_ACCEPTED) {
reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
} else {
reconnecting = false;
}
xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
} else { } else {
xmppConnectionService.removeOngoingCall(); xmppConnectionService.removeOngoingCall();
} }
@ -1507,8 +1722,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private void writeLogMessage(final State state) { private void writeLogMessage(final State state) {
final long started = this.rtpConnectionStarted; final long duration = getCallDuration();
long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started;
if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
writeLogMessageSuccess(duration); writeLogMessageSuccess(duration);
} else { } else {
@ -1553,7 +1767,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return webRTCWrapper.getRemoteVideoTrack(); return webRTCWrapper.getRemoteVideoTrack();
} }
public EglBase.Context getEglBaseContext() { public EglBase.Context getEglBaseContext() {
return webRTCWrapper.getEglBaseContext(); return webRTCWrapper.getEglBaseContext();
} }

View File

@ -1,13 +1,12 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -17,9 +16,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
@ -97,6 +96,10 @@ public class RtpContentMap {
} }
void requireDTLSFingerprint() { void requireDTLSFingerprint() {
requireDTLSFingerprint(false);
}
void requireDTLSFingerprint(final boolean requireActPass) {
if (this.contents.size() == 0) { if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available"); throw new IllegalStateException("No contents available");
} }
@ -106,9 +109,13 @@ public class RtpContentMap {
if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) {
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey()));
} }
if (Strings.isNullOrEmpty(fingerprint.getSetup())) { final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup == null) {
throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey()));
} }
if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) {
throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)");
}
} }
} }
@ -137,7 +144,56 @@ public class RtpContentMap {
final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper();
newTransportInfo.addChild(candidate); newTransportInfo.addChild(candidate);
return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo)));
}
RtpContentMap transportInfo() {
return new RtpContentMap(
null,
Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))
);
}
public IceUdpTransportInfo.Credentials getCredentials() {
final Set<IceUdpTransportInfo.Credentials> allCredentials = ImmutableSet.copyOf(Collections2.transform(
contents.values(),
dt -> dt.transport.getCredentials()
));
final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null);
if (allCredentials.size() == 1 && credentials != null) {
return credentials;
}
throw new IllegalStateException("Content map does not have distinct credentials");
}
public IceUdpTransportInfo.Setup getDtlsSetup() {
final Set<IceUdpTransportInfo.Setup> setups = ImmutableSet.copyOf(Collections2.transform(
contents.values(),
dt -> dt.transport.getFingerprint().getSetup()
));
final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null);
if (setups.size() == 1 && setup != null) {
return setup;
}
throw new IllegalStateException("Content map doesn't have distinct DTLS setup");
}
public boolean emptyCandidates() {
int count = 0;
for (DescriptionTransport descriptionTransport : contents.values()) {
count += descriptionTransport.transport.getCandidates().size();
}
return count == 0;
}
public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder = new ImmutableMap.Builder<>();
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
final RtpDescription rtpDescription = content.getValue().description;
IceUdpTransportInfo transportInfo = content.getValue().transport;
final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo));
}
return new RtpContentMap(this.group, contentMapBuilder.build());
} }
public static class DescriptionTransport { public static class DescriptionTransport {

View File

@ -4,6 +4,7 @@ public enum RtpEndUserState {
INCOMING_CALL, //received a 'propose' message INCOMING_CALL, //received a 'propose' message
CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
CONNECTED, //session-accepted and webrtc peer connection is connected CONNECTED, //session-accepted and webrtc peer connection is connected
RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
RINGING, //'propose' has been sent out and it has been 184 acked RINGING, //'propose' has been sent out and it has been 184 acked
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received

View File

@ -156,7 +156,10 @@ public class SessionDescription {
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint != null) { if (fingerprint != null) {
mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent());
mediaAttributes.put("setup", fingerprint.getSetup()); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
if (setup != null) {
mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
}
} }
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>(); final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {

View File

@ -51,7 +51,7 @@ class ToneManager {
return ToneState.ENDING_CALL; return ToneState.ENDING_CALL;
} }
} }
if (state == RtpEndUserState.CONNECTED) { if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) {
if (media.contains(Media.VIDEO)) { if (media.contains(Media.VIDEO)) {
return ToneState.NULL; return ToneState.NULL;
} else { } else {

View File

@ -17,7 +17,6 @@ import com.google.common.util.concurrent.SettableFuture;
import org.webrtc.AudioSource; import org.webrtc.AudioSource;
import org.webrtc.AudioTrack; import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator; import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraEnumerator; import org.webrtc.CameraEnumerator;
@ -45,8 +44,13 @@ import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -59,6 +63,8 @@ public class WebRTCWrapper {
private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
//we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296
private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>() private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
.add("Pixel") .add("Pixel")
@ -79,6 +85,8 @@ public class WebRTCWrapper {
private static final int CAPTURING_MAX_FRAME_RATE = 30; private static final int CAPTURING_MAX_FRAME_RATE = 30;
private final EventCallback eventCallback; private final EventCallback eventCallback;
private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
@Override @Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) { public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
@ -98,13 +106,13 @@ public class WebRTCWrapper {
} }
@Override @Override
public void onConnectionChange(PeerConnection.PeerConnectionState newState) { public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
eventCallback.onConnectionChange(newState); eventCallback.onConnectionChange(newState);
} }
@Override @Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")");
} }
@Override @Override
@ -125,7 +133,11 @@ public class WebRTCWrapper {
@Override @Override
public void onIceCandidate(IceCandidate iceCandidate) { public void onIceCandidate(IceCandidate iceCandidate) {
if (readyToReceivedIceCandidates.get()) {
eventCallback.onIceCandidate(iceCandidate); eventCallback.onIceCandidate(iceCandidate);
} else {
iceCandidates.add(iceCandidate);
}
} }
@Override @Override
@ -150,7 +162,11 @@ public class WebRTCWrapper {
@Override @Override
public void onRenegotiationNeeded() { public void onRenegotiationNeeded() {
Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState();
if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) {
eventCallback.onRenegotiationNeeded();
}
} }
@Override @Override
@ -251,11 +267,7 @@ public class WebRTCWrapper {
.createPeerConnectionFactory(); .createPeerConnectionFactory();
final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
if (peerConnection == null) { if (peerConnection == null) {
throw new InitializationException("Unable to create PeerConnection"); throw new InitializationException("Unable to create PeerConnection");
@ -289,6 +301,31 @@ public class WebRTCWrapper {
this.peerConnection = peerConnection; this.peerConnection = peerConnection;
} }
private static PeerConnection.RTCConfiguration buildConfiguration(final List<PeerConnection.IceServer> iceServers) {
final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
rtcConfig.enableImplicitRollback = true;
return rtcConfig;
}
void reconfigurePeerConnection(final List<PeerConnection.IceServer> iceServers) {
requirePeerConnection().setConfiguration(buildConfiguration(iceServers));
}
void restartIce() {
executorService.execute(() -> requirePeerConnection().restartIce());
}
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
readyToReceivedIceCandidates.set(ready);
while (ready && iceCandidates.peek() != null) {
eventCallback.onIceCandidate(iceCandidates.poll());
}
}
synchronized void close() { synchronized void close() {
final PeerConnection peerConnection = this.peerConnection; final PeerConnection peerConnection = this.peerConnection;
final CapturerChoice capturerChoice = this.capturerChoice; final CapturerChoice capturerChoice = this.capturerChoice;
@ -403,70 +440,36 @@ public class WebRTCWrapper {
videoTrack.setEnabled(enabled); videoTrack.setEnabled(enabled);
} }
ListenableFuture<SessionDescription> createOffer() { synchronized ListenableFuture<SessionDescription> setLocalDescription() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create(); final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createOffer(new CreateSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
future.set(sessionDescription);
}
@Override
public void onCreateFailure(String s) {
future.setException(new IllegalStateException("Unable to create offer: " + s));
}
}, new MediaConstraints());
return future;
}, MoreExecutors.directExecutor());
}
ListenableFuture<SessionDescription> createAnswer() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createAnswer(new CreateSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
future.set(sessionDescription);
}
@Override
public void onCreateFailure(String s) {
future.setException(new IllegalStateException("Unable to create answer: " + s));
}
}, new MediaConstraints());
return future;
}, MoreExecutors.directExecutor());
}
ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting local description:");
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line);
}
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<Void> future = SettableFuture.create();
peerConnection.setLocalDescription(new SetSdpObserver() { peerConnection.setLocalDescription(new SetSdpObserver() {
@Override @Override
public void onSetSuccess() { public void onSetSuccess() {
future.set(null); final SessionDescription description = peerConnection.getLocalDescription();
Log.d(EXTENDED_LOGGING_TAG, "set local description:");
logDescription(description);
future.set(description);
} }
@Override @Override
public void onSetFailure(final String s) { public void onSetFailure(final String message) {
future.setException(new IllegalArgumentException("unable to set local session description: " + s)); future.setException(new FailureToSetDescriptionException(message));
} }
}, sessionDescription); });
return future; return future;
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) { private static void logDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line); Log.d(EXTENDED_LOGGING_TAG, line);
} }
}
synchronized ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
logDescription(sessionDescription);
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<Void> future = SettableFuture.create(); final SettableFuture<Void> future = SettableFuture.create();
peerConnection.setRemoteDescription(new SetSdpObserver() { peerConnection.setRemoteDescription(new SetSdpObserver() {
@ -476,9 +479,8 @@ public class WebRTCWrapper {
} }
@Override @Override
public void onSetFailure(String s) { public void onSetFailure(final String message) {
future.setException(new IllegalArgumentException("unable to set remote session description: " + s)); future.setException(new FailureToSetDescriptionException(message));
} }
}, sessionDescription); }, sessionDescription);
return future; return future;
@ -489,26 +491,26 @@ public class WebRTCWrapper {
private ListenableFuture<PeerConnection> getPeerConnectionFuture() { private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
final PeerConnection peerConnection = this.peerConnection; final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) { if (peerConnection == null) {
return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first")); return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
} else { } else {
return Futures.immediateFuture(peerConnection); return Futures.immediateFuture(peerConnection);
} }
} }
private PeerConnection requirePeerConnection() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
throw new PeerConnectionNotInitialized();
}
return peerConnection;
}
void addIceCandidate(IceCandidate iceCandidate) { void addIceCandidate(IceCandidate iceCandidate) {
requirePeerConnection().addIceCandidate(iceCandidate); requirePeerConnection().addIceCandidate(iceCandidate);
} }
private CameraEnumerator getCameraEnumerator() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new Camera2Enumerator(requireContext());
} else {
return new Camera1Enumerator();
}
}
private Optional<CapturerChoice> getVideoCapturer() { private Optional<CapturerChoice> getVideoCapturer() {
final CameraEnumerator enumerator = getCameraEnumerator(); final CameraEnumerator enumerator = new Camera2Enumerator(requireContext());
final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
for (final String deviceName : deviceNames) { for (final String deviceName : deviceNames) {
if (isFrontFacing(enumerator, deviceName)) { if (isFrontFacing(enumerator, deviceName)) {
@ -527,10 +529,15 @@ public class WebRTCWrapper {
} }
} }
public PeerConnection.PeerConnectionState getState() { PeerConnection.PeerConnectionState getState() {
return requirePeerConnection().connectionState(); return requirePeerConnection().connectionState();
} }
public PeerConnection.SignalingState getSignalingState() {
return requirePeerConnection().signalingState();
}
EglBase.Context getEglBaseContext() { EglBase.Context getEglBaseContext() {
return this.eglBase.getEglBaseContext(); return this.eglBase.getEglBaseContext();
} }
@ -543,14 +550,6 @@ public class WebRTCWrapper {
return Optional.fromNullable(this.remoteVideoTrack); return Optional.fromNullable(this.remoteVideoTrack);
} }
private PeerConnection requirePeerConnection() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
throw new PeerConnectionNotInitialized();
}
return peerConnection;
}
private Context requireContext() { private Context requireContext() {
final Context context = this.context; final Context context = this.context;
if (context == null) { if (context == null) {
@ -563,12 +562,18 @@ public class WebRTCWrapper {
return appRTCAudioManager; return appRTCAudioManager;
} }
void execute(final Runnable command) {
executorService.execute(command);
}
public interface EventCallback { public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate); void onIceCandidate(IceCandidate iceCandidate);
void onConnectionChange(PeerConnection.PeerConnectionState newState); void onConnectionChange(PeerConnection.PeerConnectionState newState);
void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices); void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
void onRenegotiationNeeded();
} }
private static abstract class SetSdpObserver implements SdpObserver { private static abstract class SetSdpObserver implements SdpObserver {
@ -619,6 +624,12 @@ public class WebRTCWrapper {
} }
private static class FailureToSetDescriptionException extends IllegalArgumentException {
public FailureToSetDescriptionException(String message) {
super(message);
}
}
private static class CapturerChoice { private static class CapturerChoice {
private final CameraVideoCapturer cameraVideoCapturer; private final CameraVideoCapturer cameraVideoCapturer;
private final CameraEnumerationAndroid.CaptureFormat captureFormat; private final CameraEnumerationAndroid.CaptureFormat captureFormat;

View File

@ -1,6 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas; package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
@ -8,6 +10,8 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -58,6 +62,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); return fingerprint == null ? null : Fingerprint.upgrade(fingerprint);
} }
public Credentials getCredentials() {
final String ufrag = this.getAttribute("ufrag");
final String password = this.getAttribute("pwd");
return new Credentials(ufrag, password);
}
public List<Candidate> getCandidates() { public List<Candidate> getCandidates() {
final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>(); final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
for (final Element child : getChildren()) { for (final Element child : getChildren()) {
@ -74,6 +84,53 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return transportInfo; return transportInfo;
} }
public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) {
final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
transportInfo.setAttribute("ufrag", credentials.ufrag);
transportInfo.setAttribute("pwd", credentials.password);
for (final Element child : getChildren()) {
if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
final Fingerprint fingerprint = new Fingerprint();
fingerprint.setAttributes(new Hashtable<>(child.getAttributes()));
fingerprint.setContent(child.getContent());
fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT));
transportInfo.addChild(fingerprint);
}
}
return transportInfo;
}
public static class Credentials {
public final String ufrag;
public final String password;
public Credentials(String ufrag, String password) {
this.ufrag = ufrag;
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Credentials that = (Credentials) o;
return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password);
}
@Override
public int hashCode() {
return Objects.hashCode(ufrag, password);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("ufrag", ufrag)
.add("password", password)
.toString();
}
}
public static class Candidate extends Element { public static class Candidate extends Element {
private Candidate() { private Candidate() {
@ -89,7 +146,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
} }
// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1
public static Candidate fromSdpAttribute(final String attribute) { public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
final String[] pair = attribute.split(":", 2); final String[] pair = attribute.split(":", 2);
if (pair.length == 2 && "candidate".equals(pair[0])) { if (pair.length == 2 && "candidate".equals(pair[0])) {
final String[] segments = pair[1].split(" "); final String[] segments = pair[1].split(" ");
@ -105,6 +162,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
for (int i = 6; i < segments.length - 1; i = i + 2) { for (int i = 6; i < segments.length - 1; i = i + 2) {
additional.put(segments[i], segments[i + 1]); additional.put(segments[i], segments[i + 1]);
} }
final String ufrag = additional.get("ufrag");
if (ufrag != null && !ufrag.equals(currentUfrag)) {
return null;
}
final Candidate candidate = new Candidate(); final Candidate candidate = new Candidate();
candidate.setAttribute("component", component); candidate.setAttribute("component", component);
candidate.setAttribute("foundation", foundation); candidate.setAttribute("foundation", foundation);
@ -285,8 +346,31 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return this.getAttribute("hash"); return this.getAttribute("hash");
} }
public String getSetup() { public Setup getSetup() {
return this.getAttribute("setup"); final String setup = this.getAttribute("setup");
return setup == null ? null : Setup.of(setup);
}
}
public enum Setup {
ACTPASS, PASSIVE, ACTIVE;
public static Setup of(String setup) {
try {
return valueOf(setup.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
return null;
}
}
public Setup flip() {
if (this == PASSIVE) {
return ACTIVE;
}
if (this == ACTIVE) {
return PASSIVE;
}
throw new IllegalStateException(this.name()+" can not be flipped");
} }
} }
} }

View File

@ -904,6 +904,7 @@
<string name="rtp_state_incoming_video_call">Incoming video call</string> <string name="rtp_state_incoming_video_call">Incoming video call</string>
<string name="rtp_state_connecting">Connecting</string> <string name="rtp_state_connecting">Connecting</string>
<string name="rtp_state_connected">Connected</string> <string name="rtp_state_connected">Connected</string>
<string name="rtp_state_reconnecting">Reconnecting</string>
<string name="rtp_state_accepting_call">Accepting call</string> <string name="rtp_state_accepting_call">Accepting call</string>
<string name="rtp_state_ending_call">Ending call</string> <string name="rtp_state_ending_call">Ending call</string>
<string name="answer_call">Answer</string> <string name="answer_call">Answer</string>
@ -919,6 +920,8 @@
<string name="hang_up">Hang up</string> <string name="hang_up">Hang up</string>
<string name="ongoing_call">Ongoing call</string> <string name="ongoing_call">Ongoing call</string>
<string name="ongoing_video_call">Ongoing video call</string> <string name="ongoing_video_call">Ongoing video call</string>
<string name="reconnecting_call">Reconnecting call</string>
<string name="reconnecting_video_call">Reconnecting video call</string>
<string name="disable_tor_to_make_call">Disable Tor to make calls</string> <string name="disable_tor_to_make_call">Disable Tor to make calls</string>
<string name="incoming_call">Incoming call</string> <string name="incoming_call">Incoming call</string>
<string name="incoming_call_duration">Incoming call · %s</string> <string name="incoming_call_duration">Incoming call · %s</string>