Merge remote-tracking branch 'upstream/master' into develop
This commit is contained in:
commit
658c1c58d5
|
@ -76,7 +76,7 @@ dependencies {
|
|||
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
||||
|
||||
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 'org.webrtc:google-webrtc:1.0.32006'
|
||||
}
|
||||
|
|
|
@ -488,14 +488,23 @@ public class NotificationService {
|
|||
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");
|
||||
if (media.contains(Media.VIDEO)) {
|
||||
if (ongoingCall.media.contains(Media.VIDEO)) {
|
||||
builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
|
||||
if (ongoingCall.reconnecting) {
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call));
|
||||
} else {
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call));
|
||||
}
|
||||
} else {
|
||||
builder.setSmallIcon(R.drawable.ic_call_white_24dp);
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
|
||||
if (ongoingCall.reconnecting) {
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call));
|
||||
} else {
|
||||
builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call));
|
||||
}
|
||||
}
|
||||
builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
|
||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
|
|
|
@ -572,8 +572,8 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
|
||||
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
|
||||
public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
|
||||
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
|
||||
final String compressPictures = getCompressPicturesPreference();
|
||||
|
||||
if ("never".equals(compressPictures)
|
||||
|
@ -1298,8 +1298,8 @@ public class XmppConnectionService extends Service {
|
|||
toggleForegroundService(false);
|
||||
}
|
||||
|
||||
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
|
||||
ongoingCall.set(new OngoingCall(id, media));
|
||||
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
|
||||
ongoingCall.set(new OngoingCall(id, media, reconnecting));
|
||||
toggleForegroundService(false);
|
||||
}
|
||||
|
||||
|
@ -1315,7 +1315,7 @@ public class XmppConnectionService extends Service {
|
|||
final Notification notification;
|
||||
final int id;
|
||||
if (ongoing != null) {
|
||||
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
|
||||
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
|
||||
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
|
||||
startForeground(id, notification);
|
||||
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
|
||||
|
@ -4869,12 +4869,14 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
|
||||
public static class OngoingCall {
|
||||
private final AbstractJingleConnection.Id id;
|
||||
private final Set<Media> media;
|
||||
public final AbstractJingleConnection.Id id;
|
||||
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.media = media;
|
||||
this.reconnecting = reconnecting;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4882,12 +4884,12 @@ public class XmppConnectionService extends Service {
|
|||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
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
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(id);
|
||||
return Objects.hashCode(id, media, reconnecting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -688,14 +688,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
toggleInputMethod();
|
||||
}
|
||||
|
||||
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
||||
private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
|
||||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
|
||||
prepareFileToast.show();
|
||||
activity.delegateUriPermissionsToService(uri);
|
||||
activity.xmppConnectionService.attachImageToConversation(conversation, uri,
|
||||
activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
|
||||
new UiCallback<Message>() {
|
||||
|
||||
@Override
|
||||
|
@ -856,9 +856,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
toggleInputMethod();
|
||||
break;
|
||||
case ATTACHMENT_CHOICE_LOCATION:
|
||||
double latitude = data.getDoubleExtra("latitude", 0);
|
||||
double longitude = data.getDoubleExtra("longitude", 0);
|
||||
Uri geo = Uri.parse("geo:" + latitude + "," + longitude);
|
||||
final double latitude = data.getDoubleExtra("latitude", 0);
|
||||
final double longitude = data.getDoubleExtra("longitude", 0);
|
||||
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));
|
||||
toggleInputMethod();
|
||||
break;
|
||||
|
@ -889,7 +895,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
attachLocationToConversation(conversation, attachment.getUri());
|
||||
} else if (attachment.getType() == Attachment.Type.IMAGE) {
|
||||
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
|
||||
attachImageToConversation(conversation, attachment.getUri());
|
||||
attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
|
||||
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 pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, 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);
|
||||
if (uris != null && uris.size() > 0) {
|
||||
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
|
||||
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
|
||||
} else {
|
||||
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
|
||||
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris));
|
||||
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
|
||||
}
|
||||
toggleInputMethod();
|
||||
return;
|
||||
|
|
|
@ -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_POST_INIT_ACTION = "post_init_action";
|
||||
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(
|
||||
ACTION_VIEW_CONVERSATION,
|
||||
|
|
|
@ -96,7 +96,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
);
|
||||
private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
|
||||
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 int REQUEST_ACCEPT_CALL = 0x1111;
|
||||
|
@ -502,7 +512,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
|
||||
private boolean isConnected() {
|
||||
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() {
|
||||
|
@ -635,8 +645,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
surfaceViewRenderer.setVisibility(View.VISIBLE);
|
||||
try {
|
||||
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
|
||||
} catch (final IllegalStateException e) {
|
||||
//Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
|
||||
}
|
||||
surfaceViewRenderer.setEnableHardwareScaler(true);
|
||||
}
|
||||
|
@ -661,6 +671,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
case CONNECTED:
|
||||
setTitle(R.string.rtp_state_connected);
|
||||
break;
|
||||
case RECONNECTING:
|
||||
setTitle(R.string.rtp_state_reconnecting);
|
||||
break;
|
||||
case ACCEPTING_CALL:
|
||||
setTitle(R.string.rtp_state_accepting_call);
|
||||
break;
|
||||
|
@ -803,7 +816,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
|
||||
@SuppressLint("RestrictedApi")
|
||||
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");
|
||||
if (media.contains(Media.VIDEO)) {
|
||||
final JingleRtpConnection rtpConnection = requireRtpConnection();
|
||||
|
@ -931,14 +944,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
this.binding.duration.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
final long rtpConnectionStarted = connection.getRtpConnectionStarted();
|
||||
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 {
|
||||
if (connection.zeroDuration()) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
|
||||
if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
|
||||
binding.localVideo.setVisibility(View.GONE);
|
||||
binding.remoteVideoWrapper.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);
|
||||
binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.appBarLayout.setVisibility(View.VISIBLE);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
binding.remoteVideoWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
|
@ -13,10 +13,13 @@ import androidx.annotation.NonNull;
|
|||
import androidx.databinding.DataBindingUtil;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.math.DoubleMath;
|
||||
|
||||
import org.osmdroid.api.IGeoPoint;
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
|
||||
|
@ -28,213 +31,213 @@ import eu.siacs.conversations.utils.ThemeHelper;
|
|||
|
||||
public class ShareLocationActivity extends LocationActivity implements LocationListener {
|
||||
|
||||
private Snackbar snackBar;
|
||||
private ActivityShareLocationBinding binding;
|
||||
private boolean marker_fixed_to_loc = false;
|
||||
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
|
||||
private Boolean noAskAgain = false;
|
||||
private Snackbar snackBar;
|
||||
private ActivityShareLocationBinding binding;
|
||||
private boolean marker_fixed_to_loc = false;
|
||||
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
|
||||
private Boolean noAskAgain = false;
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
|
||||
}
|
||||
outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
|
||||
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
|
||||
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
|
||||
}
|
||||
}
|
||||
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
|
||||
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
|
||||
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location);
|
||||
setSupportActionBar(binding.toolbar);
|
||||
configureActionBar(getSupportActionBar());
|
||||
setupMapView(binding.map, LocationProvider.getGeoPoint(this));
|
||||
|
||||
this.binding.cancelButton.setOnClickListener(view -> {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
});
|
||||
this.binding.cancelButton.setOnClickListener(view -> {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
});
|
||||
|
||||
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
|
||||
this.snackBar.setAction(R.string.enable, view -> {
|
||||
if (isLocationEnabledAndAllowed()) {
|
||||
updateUi();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
|
||||
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
|
||||
} else if (!isLocationEnabled()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
}
|
||||
});
|
||||
ThemeHelper.fix(this.snackBar);
|
||||
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
|
||||
this.snackBar.setAction(R.string.enable, view -> {
|
||||
if (isLocationEnabledAndAllowed()) {
|
||||
updateUi();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
|
||||
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
|
||||
} else if (!isLocationEnabled()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
}
|
||||
});
|
||||
ThemeHelper.fix(this.snackBar);
|
||||
|
||||
this.binding.shareButton.setOnClickListener(view -> {
|
||||
final Intent result = new Intent();
|
||||
this.binding.shareButton.setOnClickListener(this::shareLocation);
|
||||
|
||||
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());
|
||||
}
|
||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
|
||||
|
||||
setResult(RESULT_OK, result);
|
||||
finish();
|
||||
});
|
||||
this.binding.fab.setOnClickListener(view -> {
|
||||
if (!marker_fixed_to_loc) {
|
||||
if (!isLocationEnabled()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(REQUEST_CODE_FAB_PRESSED);
|
||||
}
|
||||
}
|
||||
toggleFixedLocation();
|
||||
});
|
||||
}
|
||||
|
||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
|
||||
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();
|
||||
}
|
||||
|
||||
this.binding.fab.setOnClickListener(view -> {
|
||||
if (!marker_fixed_to_loc) {
|
||||
if (!isLocationEnabled()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(REQUEST_CODE_FAB_PRESSED);
|
||||
}
|
||||
}
|
||||
toggleFixedLocation();
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length > 0 &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
Build.VERSION.SDK_INT >= 23 &&
|
||||
permissions.length > 0 &&
|
||||
(
|
||||
Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
|
||||
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
|
||||
) &&
|
||||
!shouldShowRequestPermissionRationale(permissions[0])) {
|
||||
noAskAgain = true;
|
||||
}
|
||||
|
||||
if (grantResults.length > 0 &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
Build.VERSION.SDK_INT >= 23 &&
|
||||
permissions.length > 0 &&
|
||||
(
|
||||
Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
|
||||
Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
|
||||
) &&
|
||||
!shouldShowRequestPermissionRationale(permissions[0])) {
|
||||
noAskAgain = true;
|
||||
}
|
||||
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
}
|
||||
updateUi();
|
||||
}
|
||||
|
||||
if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
}
|
||||
updateUi();
|
||||
}
|
||||
@Override
|
||||
protected void gotoLoc(final boolean setZoomLevel) {
|
||||
if (this.myLoc != null && mapController != null) {
|
||||
if (setZoomLevel) {
|
||||
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
|
||||
}
|
||||
mapController.animateTo(new GeoPoint(this.myLoc));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void gotoLoc(final boolean setZoomLevel) {
|
||||
if (this.myLoc != null && mapController != null) {
|
||||
if (setZoomLevel) {
|
||||
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
|
||||
}
|
||||
mapController.animateTo(new GeoPoint(this.myLoc));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void setMyLoc(final Location location) {
|
||||
this.myLoc = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setMyLoc(final Location location) {
|
||||
this.myLoc = location;
|
||||
}
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
@Override
|
||||
protected void updateLocationMarkers() {
|
||||
super.updateLocationMarkers();
|
||||
if (this.myLoc != null) {
|
||||
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
|
||||
if (this.marker_fixed_to_loc) {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
|
||||
} else {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||
}
|
||||
} else {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateLocationMarkers() {
|
||||
super.updateLocationMarkers();
|
||||
if (this.myLoc != null) {
|
||||
this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
|
||||
if (this.marker_fixed_to_loc) {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
|
||||
} else {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||
}
|
||||
} else {
|
||||
this.binding.map.getOverlays().add(new Marker(marker_icon));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
if (this.myLoc == null) {
|
||||
this.marker_fixed_to_loc = true;
|
||||
}
|
||||
updateUi();
|
||||
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
|
||||
final Location oldLoc = this.myLoc;
|
||||
this.myLoc = location;
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
if (this.myLoc == null) {
|
||||
this.marker_fixed_to_loc = true;
|
||||
}
|
||||
updateUi();
|
||||
if (LocationHelper.isBetterLocation(location, this.myLoc)) {
|
||||
final Location oldLoc = this.myLoc;
|
||||
this.myLoc = location;
|
||||
// Don't jump back to the users location if they're not moving (more or less).
|
||||
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
|
||||
gotoLoc();
|
||||
}
|
||||
|
||||
// Don't jump back to the users location if they're not moving (more or less).
|
||||
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
|
||||
gotoLoc();
|
||||
}
|
||||
updateLocationMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
updateLocationMarkers();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onProviderEnabled(final String provider) {
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(final String provider) {
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onProviderDisabled(final String provider) {
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(final String provider) {
|
||||
}
|
||||
|
||||
}
|
||||
private boolean isLocationEnabledAndAllowed() {
|
||||
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
|
||||
}
|
||||
|
||||
private boolean isLocationEnabledAndAllowed() {
|
||||
return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
|
||||
}
|
||||
private void toggleFixedLocation() {
|
||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
|
||||
if (this.marker_fixed_to_loc) {
|
||||
gotoLoc(false);
|
||||
}
|
||||
updateLocationMarkers();
|
||||
updateUi();
|
||||
}
|
||||
|
||||
private void toggleFixedLocation() {
|
||||
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
|
||||
if (this.marker_fixed_to_loc) {
|
||||
gotoLoc(false);
|
||||
}
|
||||
updateLocationMarkers();
|
||||
updateUi();
|
||||
}
|
||||
@Override
|
||||
protected void updateUi() {
|
||||
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
|
||||
this.snackBar.dismiss();
|
||||
} else {
|
||||
this.snackBar.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateUi() {
|
||||
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
|
||||
this.snackBar.dismiss();
|
||||
} else {
|
||||
this.snackBar.show();
|
||||
}
|
||||
|
||||
if (isLocationEnabledAndAllowed()) {
|
||||
this.binding.fab.setVisibility(View.VISIBLE);
|
||||
runOnUiThread(() -> {
|
||||
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
|
||||
R.drawable.ic_gps_not_fixed_white_24dp);
|
||||
this.binding.fab.setContentDescription(getResources().getString(
|
||||
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
|
||||
));
|
||||
this.binding.fab.invalidate();
|
||||
});
|
||||
} else {
|
||||
this.binding.fab.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
if (isLocationEnabledAndAllowed()) {
|
||||
this.binding.fab.setVisibility(View.VISIBLE);
|
||||
runOnUiThread(() -> {
|
||||
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
|
||||
R.drawable.ic_gps_not_fixed_white_24dp);
|
||||
this.binding.fab.setContentDescription(getResources().getString(
|
||||
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
|
||||
));
|
||||
this.binding.fab.invalidate();
|
||||
});
|
||||
} else {
|
||||
this.binding.fab.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
|||
refreshUi();
|
||||
}
|
||||
|
||||
private class Share {
|
||||
private static class Share {
|
||||
public String type;
|
||||
ArrayList<Uri> uris = new ArrayList<>();
|
||||
public String account;
|
||||
public String contact;
|
||||
|
@ -65,6 +66,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
|||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length > 0)
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (requestCode == REQUEST_STORAGE_PERMISSION) {
|
||||
|
@ -139,6 +141,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
|||
} else if (type != null && uri != null) {
|
||||
this.share.uris.clear();
|
||||
this.share.uris.add(uri);
|
||||
this.share.type = type;
|
||||
} else {
|
||||
this.share.text = text;
|
||||
this.share.asQuote = asQuote;
|
||||
|
@ -193,6 +196,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
|
|||
intent.setAction(Intent.ACTION_SEND_MULTIPLE);
|
||||
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
if (share.type != null) {
|
||||
intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type);
|
||||
}
|
||||
} else if (share.text != null) {
|
||||
intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, share.text);
|
||||
|
|
|
@ -136,10 +136,10 @@ public class Attachment implements Parcelable {
|
|||
return Collections.singletonList(new Attachment(uri, type, mime));
|
||||
}
|
||||
|
||||
public static List<Attachment> of(final Context context, List<Uri> uris) {
|
||||
List<Attachment> attachments = new ArrayList<>();
|
||||
for (Uri uri : uris) {
|
||||
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri);
|
||||
public static List<Attachment> of(final Context context, List<Uri> uris, final String type) {
|
||||
final List<Attachment> attachments = new ArrayList<>();
|
||||
for (final Uri uri : uris) {
|
||||
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type);
|
||||
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
|
||||
}
|
||||
return attachments;
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.content.Context;
|
|||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
|
@ -16,11 +18,14 @@ import eu.siacs.conversations.R;
|
|||
|
||||
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 {
|
||||
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();
|
||||
if (simCountry != null && simCountry.length() == 2) { // SIM country code is available
|
||||
return simCountry.toUpperCase(Locale.US);
|
||||
|
@ -30,40 +35,41 @@ public class LocationProvider {
|
|||
return networkCountry.toUpperCase(Locale.US);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// fallthrough
|
||||
return getUserCountryFallback();
|
||||
} catch (final Exception e) {
|
||||
return getUserCountryFallback();
|
||||
}
|
||||
Locale locale = Locale.getDefault();
|
||||
}
|
||||
|
||||
private static String getUserCountryFallback() {
|
||||
final Locale locale = Locale.getDefault();
|
||||
return locale.getCountry();
|
||||
}
|
||||
|
||||
public static GeoPoint getGeoPoint(Context context) {
|
||||
public static GeoPoint getGeoPoint(final Context context) {
|
||||
return getGeoPoint(context, getUserCountry(context));
|
||||
}
|
||||
|
||||
|
||||
public static synchronized GeoPoint getGeoPoint(Context context, String country) {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)));
|
||||
public static synchronized GeoPoint getGeoPoint(final Context context, final String country) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) {
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
String[] parts = line.split("\\s+",4);
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final String[] parts = line.split("\\s+", 4);
|
||||
if (parts.length == 4) {
|
||||
if (country.equalsIgnoreCase(parts[0])) {
|
||||
try {
|
||||
return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
|
||||
} catch (NumberFormatException e) {
|
||||
} catch (final NumberFormatException e) {
|
||||
return FALLBACK;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"unable to parse line="+line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG,e.getMessage());
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to parse country->geo map", e);
|
||||
}
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -71,10 +71,14 @@ public class TimeFrameUtils {
|
|||
|
||||
public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) {
|
||||
final long passed = (since < 0) ? 0 : (to - since);
|
||||
final int hours = (int) (passed / 3600000);
|
||||
final int minutes = (int) (passed / 60000) % 60;
|
||||
final int seconds = (int) (passed / 1000) % 60;
|
||||
final int milliseconds = (int) (passed / 100) % 10;
|
||||
return formatElapsedTime(passed, withMilliseconds);
|
||||
}
|
||||
|
||||
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) {
|
||||
return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds);
|
||||
} else if (withMilliseconds) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.siacs.conversations.xml;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
@ -165,8 +167,9 @@ public class Element {
|
|||
return this.attributes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String toString() {
|
||||
StringBuilder elementOutput = new StringBuilder();
|
||||
final StringBuilder elementOutput = new StringBuilder();
|
||||
if ((content == null) && (children.size() == 0)) {
|
||||
Tag emptyTag = Tag.empty(name);
|
||||
emptyTag.setAtttributes(this.attributes);
|
||||
|
|
|
@ -28,6 +28,7 @@ public final class Namespace {
|
|||
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 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_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
|
||||
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";
|
||||
|
|
|
@ -54,7 +54,6 @@ import javax.net.ssl.X509TrustManager;
|
|||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.DomainHostnameVerifier;
|
||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.sasl.Anonymous;
|
||||
|
|
|
@ -206,7 +206,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
final Element error = response.addChild("error");
|
||||
error.setAttribute("type", conditionType);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -8,6 +7,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Collections2;
|
||||
|
@ -25,13 +25,15 @@ import org.webrtc.IceCandidate;
|
|||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
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 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 Message message;
|
||||
private State state = State.NULL;
|
||||
|
@ -147,8 +149,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
private Set<Media> proposedMedia;
|
||||
private RtpContentMap initiatorRtpContentMap;
|
||||
private RtpContentMap responderRtpContentMap;
|
||||
private long rtpConnectionStarted = 0; //time of 'connected'
|
||||
private long rtpConnectionEnded = 0;
|
||||
private IceUdpTransportInfo.Setup peerDtlsSetup;
|
||||
private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
|
||||
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
||||
private ScheduledFuture<?> ringingTimeoutFuture;
|
||||
|
||||
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
|
||||
|
@ -190,7 +193,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
|
||||
@Override
|
||||
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
|
||||
switch (jinglePacket.getAction()) {
|
||||
case SESSION_INITIATE:
|
||||
receiveSessionInitiate(jinglePacket);
|
||||
|
@ -251,24 +253,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
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
|
||||
if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
|
||||
respondOk(jinglePacket);
|
||||
final RtpContentMap contentMap;
|
||||
try {
|
||||
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);
|
||||
respondOk(jinglePacket);
|
||||
return;
|
||||
}
|
||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
||||
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);
|
||||
}
|
||||
receiveTransportInfo(jinglePacket, contentMap);
|
||||
} else {
|
||||
if (isTerminated()) {
|
||||
respondOk(jinglePacket);
|
||||
|
@ -280,37 +273,161 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
}
|
||||
|
||||
private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) {
|
||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
||||
if (this.state == State.SESSION_ACCEPTED) {
|
||||
//zero candidates + modified credentials are an ICE restart offer
|
||||
if (checkForIceRestart(jinglePacket, contentMap)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
||||
processCandidate(content);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
sdp = candidate.toSdpAttribute(credentials.ufrag);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
final int mLineIndex = indices.indexOf(sdpMid);
|
||||
if (mLineIndex < 0) {
|
||||
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
|
||||
}
|
||||
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
|
||||
Log.d(Config.LOGTAG, "received candidate: " + 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");
|
||||
}
|
||||
processCandidates(identificationTags, contents);
|
||||
}
|
||||
|
||||
private void processCandidates(final List<String> indices, final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
||||
final String ufrag = content.getValue().transport.getAttribute("ufrag");
|
||||
for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) {
|
||||
final String sdp;
|
||||
try {
|
||||
sdp = candidate.toSdpAttribute(ufrag);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
final String sdpMid = content.getKey();
|
||||
final int mLineIndex = indices.indexOf(sdpMid);
|
||||
if (mLineIndex < 0) {
|
||||
Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices);
|
||||
}
|
||||
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
|
||||
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
|
||||
this.webRTCWrapper.addIceCandidate(iceCandidate);
|
||||
}
|
||||
}
|
||||
return identificationTags;
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
contentMap.requireContentDescriptions();
|
||||
contentMap.requireDTLSFingerprint();
|
||||
contentMap.requireDTLSFingerprint(true);
|
||||
} catch (final RuntimeException e) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e));
|
||||
respondOk(jinglePacket);
|
||||
|
@ -398,11 +515,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
|
||||
respondOk(jinglePacket);
|
||||
|
||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
|
||||
if (candidates.size() > 0) {
|
||||
pendingIceCandidates.push(candidates);
|
||||
}
|
||||
pendingIceCandidates.addAll(contentMap.contents.entrySet());
|
||||
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
|
||||
sendSessionAccept();
|
||||
|
@ -471,6 +584,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
|
||||
private void receiveSessionAccept(final RtpContentMap contentMap) {
|
||||
this.responderRtpContentMap = contentMap;
|
||||
this.storePeerDtlsSetup(contentMap.getDtlsSetup());
|
||||
final SessionDescription sessionDescription;
|
||||
try {
|
||||
sessionDescription = SessionDescription.of(contentMap);
|
||||
|
@ -489,11 +603,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e));
|
||||
webRTCWrapper.close();
|
||||
sendSessionTerminate(Reason.FAILED_APPLICATION);
|
||||
sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage());
|
||||
return;
|
||||
}
|
||||
final List<String> identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags();
|
||||
processCandidates(identificationTags, contentMap.contents.entrySet());
|
||||
processCandidates(contentMap.contents.entrySet());
|
||||
}
|
||||
|
||||
private void sendSessionAccept() {
|
||||
|
@ -537,7 +650,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
try {
|
||||
this.webRTCWrapper.setRemoteDescription(sdp).get();
|
||||
addIceCandidatesFromBlackLog();
|
||||
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
|
||||
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get();
|
||||
prepareSessionAccept(webRTCSessionDescription);
|
||||
} catch (final Exception e) {
|
||||
failureToAcceptSession(e);
|
||||
|
@ -548,15 +661,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
if (isTerminated()) {
|
||||
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();
|
||||
sendSessionTerminate(Reason.ofThrowable(throwable));
|
||||
sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
|
||||
}
|
||||
|
||||
private void addIceCandidatesFromBlackLog() {
|
||||
while (!this.pendingIceCandidates.isEmpty()) {
|
||||
processCandidates(this.pendingIceCandidates.poll());
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log");
|
||||
Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
|
||||
while ((foo = this.pendingIceCandidates.poll()) != null) {
|
||||
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 RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
|
||||
this.responderRtpContentMap = respondingRtpContentMap;
|
||||
storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
|
||||
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
|
||||
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap);
|
||||
Futures.addCallback(outgoingContentMapFuture,
|
||||
new FutureCallback<RtpContentMap>() {
|
||||
@Override
|
||||
public void onSuccess(final RtpContentMap outgoingContentMap) {
|
||||
sendSessionAccept(outgoingContentMap, webRTCSessionDescription);
|
||||
sendSessionAccept(outgoingContentMap);
|
||||
}
|
||||
|
||||
@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()) {
|
||||
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do.");
|
||||
return;
|
||||
|
@ -589,11 +706,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
transitionOrThrow(State.SESSION_ACCEPTED);
|
||||
final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
|
||||
send(sessionAccept);
|
||||
try {
|
||||
webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
|
||||
} catch (Exception e) {
|
||||
failureToAcceptSession(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) {
|
||||
|
@ -841,9 +953,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
return;
|
||||
}
|
||||
try {
|
||||
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
|
||||
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get();
|
||||
prepareSessionInitiate(webRTCSessionDescription, targetState);
|
||||
} catch (final Exception e) {
|
||||
//TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions
|
||||
failureToInitiateSession(e, targetState);
|
||||
}
|
||||
}
|
||||
|
@ -873,11 +986,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
|
||||
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
|
||||
this.initiatorRtpContentMap = rtpContentMap;
|
||||
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
|
||||
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap);
|
||||
Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() {
|
||||
@Override
|
||||
public void onSuccess(final RtpContentMap outgoingContentMap) {
|
||||
sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState);
|
||||
sendSessionInitiate(outgoingContentMap, targetState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -887,7 +1001,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}, 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()) {
|
||||
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do.");
|
||||
return;
|
||||
|
@ -895,11 +1009,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
this.transitionOrThrow(targetState);
|
||||
final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
|
||||
send(sessionInitiate);
|
||||
try {
|
||||
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
|
||||
} catch (Exception e) {
|
||||
failureToInitiateSession(e, targetState);
|
||||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<RtpContentMap> encryptSessionInitiate(final RtpContentMap rtpContentMap) {
|
||||
|
@ -965,36 +1074,48 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
|
||||
private synchronized void handleIqResponse(final Account account, final IqPacket response) {
|
||||
if (response.getType() == IqPacket.TYPE.ERROR) {
|
||||
final String errorCondition = response.getErrorCondition();
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition);
|
||||
if (isTerminated()) {
|
||||
Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
final State target;
|
||||
if (Arrays.asList(
|
||||
"service-unavailable",
|
||||
"recipient-unavailable",
|
||||
"remote-server-not-found",
|
||||
"remote-server-timeout"
|
||||
).contains(errorCondition)) {
|
||||
target = State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
} else {
|
||||
target = State.TERMINATED_APPLICATION_FAILURE;
|
||||
}
|
||||
transitionOrThrow(target);
|
||||
this.finish();
|
||||
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error");
|
||||
if (isTerminated()) {
|
||||
Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
|
||||
this.finish();
|
||||
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();
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition);
|
||||
if (isTerminated()) {
|
||||
Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
final State target;
|
||||
if (Arrays.asList(
|
||||
"service-unavailable",
|
||||
"recipient-unavailable",
|
||||
"remote-server-not-found",
|
||||
"remote-server-timeout"
|
||||
).contains(errorCondition)) {
|
||||
target = State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
} else {
|
||||
target = State.TERMINATED_APPLICATION_FAILURE;
|
||||
}
|
||||
transitionOrThrow(target);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
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");
|
||||
if (isTerminated()) {
|
||||
Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
|
||||
|
@ -1005,8 +1126,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
this.finish();
|
||||
}
|
||||
|
||||
private void respondWithTieBreak(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -1043,24 +1172,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
return RtpEndUserState.CONNECTING;
|
||||
}
|
||||
case SESSION_ACCEPTED:
|
||||
//TODO refactor this out into separate method (that uses switch for better readability)
|
||||
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;
|
||||
}
|
||||
return getPeerConnectionStateAsEndUserState();
|
||||
case REJECTED:
|
||||
case REJECTED_RACED:
|
||||
case TERMINATED_DECLINED_OR_BUSY:
|
||||
|
@ -1081,7 +1193,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
return RtpEndUserState.RETRACTED;
|
||||
}
|
||||
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:
|
||||
return RtpEndUserState.APPLICATION_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));
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
final State current = getState();
|
||||
if (current == State.NULL) {
|
||||
|
@ -1332,7 +1467,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
|
||||
@Override
|
||||
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());
|
||||
sendTransportInfo(iceCandidate.sdpMid, candidate);
|
||||
}
|
||||
|
@ -1340,30 +1481,97 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
@Override
|
||||
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
|
||||
if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) {
|
||||
this.rtpConnectionStarted = SystemClock.elapsedRealtime();
|
||||
this.stateHistory.add(newState);
|
||||
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);
|
||||
|
||||
if (newState == PeerConnection.PeerConnectionState.FAILED) {
|
||||
if (neverConnected) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
|
||||
return;
|
||||
} else {
|
||||
webRTCWrapper.restartIce();
|
||||
}
|
||||
}
|
||||
//TODO 'failed' means we need to restart ICE
|
||||
//
|
||||
//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()) {
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state);
|
||||
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;
|
||||
}
|
||||
new Thread(this::closeWebRTCSessionAfterFailedConnection).start();
|
||||
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 {
|
||||
updateEndUserState();
|
||||
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() {
|
||||
this.webRTCWrapper.close();
|
||||
synchronized (this) {
|
||||
|
@ -1375,12 +1583,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
}
|
||||
|
||||
public long getRtpConnectionStarted() {
|
||||
return this.rtpConnectionStarted;
|
||||
public boolean zeroDuration() {
|
||||
return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0;
|
||||
}
|
||||
|
||||
public long getRtpConnectionEnded() {
|
||||
return this.rtpConnectionEnded;
|
||||
public long getCallDuration() {
|
||||
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public AppRTCAudioManager getAudioManager() {
|
||||
|
@ -1427,8 +1635,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
|
||||
private void updateOngoingCallNotification() {
|
||||
if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) {
|
||||
xmppConnectionService.setOngoingCall(id, getMedia());
|
||||
final State state = this.state;
|
||||
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 {
|
||||
xmppConnectionService.removeOngoingCall();
|
||||
}
|
||||
|
@ -1507,8 +1722,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
}
|
||||
|
||||
private void writeLogMessage(final State state) {
|
||||
final long started = this.rtpConnectionStarted;
|
||||
long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started;
|
||||
final long duration = getCallDuration();
|
||||
if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
|
||||
writeLogMessageSuccess(duration);
|
||||
} else {
|
||||
|
@ -1553,7 +1767,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
return webRTCWrapper.getRemoteVideoTrack();
|
||||
}
|
||||
|
||||
|
||||
public EglBase.Context getEglBaseContext() {
|
||||
return webRTCWrapper.getEglBaseContext();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
@ -17,9 +16,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
|||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
|
@ -97,6 +96,10 @@ public class RtpContentMap {
|
|||
}
|
||||
|
||||
void requireDTLSFingerprint() {
|
||||
requireDTLSFingerprint(false);
|
||||
}
|
||||
|
||||
void requireDTLSFingerprint(final boolean requireActPass) {
|
||||
if (this.contents.size() == 0) {
|
||||
throw new IllegalStateException("No contents available");
|
||||
}
|
||||
|
@ -106,9 +109,13 @@ public class RtpContentMap {
|
|||
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()));
|
||||
}
|
||||
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()));
|
||||
}
|
||||
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();
|
||||
newTransportInfo.addChild(candidate);
|
||||
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 {
|
||||
|
|
|
@ -4,6 +4,7 @@ public enum RtpEndUserState {
|
|||
INCOMING_CALL, //received a 'propose' message
|
||||
CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
|
||||
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
|
||||
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
|
||||
|
|
|
@ -156,7 +156,10 @@ public class SessionDescription {
|
|||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||
if (fingerprint != null) {
|
||||
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<>();
|
||||
for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
|
||||
|
|
|
@ -51,7 +51,7 @@ class ToneManager {
|
|||
return ToneState.ENDING_CALL;
|
||||
}
|
||||
}
|
||||
if (state == RtpEndUserState.CONNECTED) {
|
||||
if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) {
|
||||
if (media.contains(Media.VIDEO)) {
|
||||
return ToneState.NULL;
|
||||
} else {
|
||||
|
|
|
@ -17,7 +17,6 @@ import com.google.common.util.concurrent.SettableFuture;
|
|||
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.Camera1Enumerator;
|
||||
import org.webrtc.Camera2Enumerator;
|
||||
import org.webrtc.CameraEnumerationAndroid;
|
||||
import org.webrtc.CameraEnumerator;
|
||||
|
@ -45,8 +44,13 @@ import org.webrtc.voiceengine.WebRtcAudioEffects;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
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.Nullable;
|
||||
|
@ -59,6 +63,8 @@ public class WebRTCWrapper {
|
|||
|
||||
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
|
||||
private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
|
||||
.add("Pixel")
|
||||
|
@ -79,6 +85,8 @@ public class WebRTCWrapper {
|
|||
private static final int CAPTURING_MAX_FRAME_RATE = 30;
|
||||
|
||||
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() {
|
||||
@Override
|
||||
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
|
||||
|
@ -98,13 +106,13 @@ public class WebRTCWrapper {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
|
||||
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
|
||||
eventCallback.onConnectionChange(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
|
||||
Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -125,7 +133,11 @@ public class WebRTCWrapper {
|
|||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate iceCandidate) {
|
||||
eventCallback.onIceCandidate(iceCandidate);
|
||||
if (readyToReceivedIceCandidates.get()) {
|
||||
eventCallback.onIceCandidate(iceCandidate);
|
||||
} else {
|
||||
iceCandidates.add(iceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -150,7 +162,11 @@ public class WebRTCWrapper {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -251,11 +267,7 @@ public class WebRTCWrapper {
|
|||
.createPeerConnectionFactory();
|
||||
|
||||
|
||||
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;
|
||||
final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers);
|
||||
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
|
||||
if (peerConnection == null) {
|
||||
throw new InitializationException("Unable to create PeerConnection");
|
||||
|
@ -289,6 +301,31 @@ public class WebRTCWrapper {
|
|||
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() {
|
||||
final PeerConnection peerConnection = this.peerConnection;
|
||||
final CapturerChoice capturerChoice = this.capturerChoice;
|
||||
|
@ -403,70 +440,36 @@ public class WebRTCWrapper {
|
|||
videoTrack.setEnabled(enabled);
|
||||
}
|
||||
|
||||
ListenableFuture<SessionDescription> createOffer() {
|
||||
synchronized ListenableFuture<SessionDescription> setLocalDescription() {
|
||||
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
|
||||
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() {
|
||||
@Override
|
||||
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
|
||||
public void onSetFailure(final String s) {
|
||||
future.setException(new IllegalArgumentException("unable to set local session description: " + s));
|
||||
|
||||
public void onSetFailure(final String message) {
|
||||
future.setException(new FailureToSetDescriptionException(message));
|
||||
}
|
||||
}, sessionDescription);
|
||||
});
|
||||
return future;
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
|
||||
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
|
||||
private static void logDescription(final SessionDescription sessionDescription) {
|
||||
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
|
||||
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 -> {
|
||||
final SettableFuture<Void> future = SettableFuture.create();
|
||||
peerConnection.setRemoteDescription(new SetSdpObserver() {
|
||||
|
@ -476,9 +479,8 @@ public class WebRTCWrapper {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
future.setException(new IllegalArgumentException("unable to set remote session description: " + s));
|
||||
|
||||
public void onSetFailure(final String message) {
|
||||
future.setException(new FailureToSetDescriptionException(message));
|
||||
}
|
||||
}, sessionDescription);
|
||||
return future;
|
||||
|
@ -489,26 +491,26 @@ public class WebRTCWrapper {
|
|||
private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
|
||||
final PeerConnection peerConnection = this.peerConnection;
|
||||
if (peerConnection == null) {
|
||||
return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first"));
|
||||
return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
|
||||
} else {
|
||||
return Futures.immediateFuture(peerConnection);
|
||||
}
|
||||
}
|
||||
|
||||
private PeerConnection requirePeerConnection() {
|
||||
final PeerConnection peerConnection = this.peerConnection;
|
||||
if (peerConnection == null) {
|
||||
throw new PeerConnectionNotInitialized();
|
||||
}
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
void addIceCandidate(IceCandidate 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() {
|
||||
final CameraEnumerator enumerator = getCameraEnumerator();
|
||||
final CameraEnumerator enumerator = new Camera2Enumerator(requireContext());
|
||||
final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
|
||||
for (final String deviceName : deviceNames) {
|
||||
if (isFrontFacing(enumerator, deviceName)) {
|
||||
|
@ -527,10 +529,15 @@ public class WebRTCWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
public PeerConnection.PeerConnectionState getState() {
|
||||
PeerConnection.PeerConnectionState getState() {
|
||||
return requirePeerConnection().connectionState();
|
||||
}
|
||||
|
||||
public PeerConnection.SignalingState getSignalingState() {
|
||||
return requirePeerConnection().signalingState();
|
||||
}
|
||||
|
||||
|
||||
EglBase.Context getEglBaseContext() {
|
||||
return this.eglBase.getEglBaseContext();
|
||||
}
|
||||
|
@ -543,14 +550,6 @@ public class WebRTCWrapper {
|
|||
return Optional.fromNullable(this.remoteVideoTrack);
|
||||
}
|
||||
|
||||
private PeerConnection requirePeerConnection() {
|
||||
final PeerConnection peerConnection = this.peerConnection;
|
||||
if (peerConnection == null) {
|
||||
throw new PeerConnectionNotInitialized();
|
||||
}
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
private Context requireContext() {
|
||||
final Context context = this.context;
|
||||
if (context == null) {
|
||||
|
@ -563,12 +562,18 @@ public class WebRTCWrapper {
|
|||
return appRTCAudioManager;
|
||||
}
|
||||
|
||||
void execute(final Runnable command) {
|
||||
executorService.execute(command);
|
||||
}
|
||||
|
||||
public interface EventCallback {
|
||||
void onIceCandidate(IceCandidate iceCandidate);
|
||||
|
||||
void onConnectionChange(PeerConnection.PeerConnectionState newState);
|
||||
|
||||
void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
|
||||
|
||||
void onRenegotiationNeeded();
|
||||
}
|
||||
|
||||
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 final CameraVideoCapturer cameraVideoCapturer;
|
||||
private final CameraEnumerationAndroid.CaptureFormat captureFormat;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
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.Strings;
|
||||
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.Iterables;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -58,6 +62,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
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() {
|
||||
final ImmutableList.Builder<Candidate> builder = new ImmutableList.Builder<>();
|
||||
for (final Element child : getChildren()) {
|
||||
|
@ -74,6 +84,53 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
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 {
|
||||
|
||||
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
|
||||
public static Candidate fromSdpAttribute(final String attribute) {
|
||||
public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
|
||||
final String[] pair = attribute.split(":", 2);
|
||||
if (pair.length == 2 && "candidate".equals(pair[0])) {
|
||||
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) {
|
||||
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();
|
||||
candidate.setAttribute("component", component);
|
||||
candidate.setAttribute("foundation", foundation);
|
||||
|
@ -285,8 +346,31 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
return this.getAttribute("hash");
|
||||
}
|
||||
|
||||
public String getSetup() {
|
||||
return this.getAttribute("setup");
|
||||
public Setup getSetup() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -904,6 +904,7 @@
|
|||
<string name="rtp_state_incoming_video_call">Incoming video call</string>
|
||||
<string name="rtp_state_connecting">Connecting</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_ending_call">Ending call</string>
|
||||
<string name="answer_call">Answer</string>
|
||||
|
@ -919,6 +920,8 @@
|
|||
<string name="hang_up">Hang up</string>
|
||||
<string name="ongoing_call">Ongoing 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="incoming_call">Incoming call</string>
|
||||
<string name="incoming_call_duration">Incoming call · %s</string>
|
||||
|
|
Loading…
Reference in New Issue