Compare commits
27 Commits
2.10.2-sum
...
develop
Author | SHA1 | Date |
---|---|---|
|
aa8b9f338f | |
|
658c1c58d5 | |
|
39792f0815 | |
![]() |
db834a1f07 | |
![]() |
f8a94161db | |
![]() |
5d526a77e3 | |
![]() |
a508a81553 | |
![]() |
61fb38cd84 | |
![]() |
1bf2d5dd8f | |
![]() |
0a18c8613f | |
![]() |
abb671616c | |
![]() |
297a843b9c | |
![]() |
0698fa0d8c | |
![]() |
70b5d8d81a | |
![]() |
0a3947b8e3 | |
![]() |
3f402b132b | |
![]() |
5b80c62a63 | |
![]() |
717c83753f | |
![]() |
b6dee6da6a | |
![]() |
9c3f55bef2 | |
![]() |
9843b72f6f | |
![]() |
61851e5f84 | |
![]() |
4ec0996dff | |
![]() |
fda45a7c86 | |
![]() |
b5786787f0 | |
![]() |
d4cbf2e11e | |
![]() |
7d7e158fd7 |
16
build.gradle
16
build.gradle
|
@ -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'
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 42023
|
||||
versionCode 4202301
|
||||
versionName "2.10.2"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.sum7.conversations"
|
||||
|
@ -262,16 +262,4 @@ android {
|
|||
exclude 'META-INF/BCKEY.DSA'
|
||||
exclude 'META-INF/BCKEY.SF'
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
|
||||
if (baseAbiVersionCode != null) {
|
||||
output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode
|
||||
} else {
|
||||
output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
• Fix usage directTLS of manuelle enter an address
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
|
||||
<string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
|
||||
<string name="use_chat.sum7.eu">chat.sum7.eu ব্যবহার করা যাক</string>
|
||||
<string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
|
||||
<string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
|
||||
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
|
||||
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই chat.sum7.eu -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
|
||||
<string name="magic_create_text_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
|
||||
<string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
|
||||
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Vælg din XMPP-udbyder</string>
|
||||
<string name="use_conversations.im">Brug conversations.im</string>
|
||||
<string name="use_chat.sum7.eu">Brug chat.sum7.eu</string>
|
||||
<string name="create_new_account">Opret ny konto</string>
|
||||
<string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
|
||||
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im¹; en udbyder, der er specielt velegnet til brug med Conversations.</string>
|
||||
<string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på chat.sum7.eu; en udbyder, der er specielt velegnet til brug med Conversations.</string>
|
||||
<string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
||||
<string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
|
||||
<string name="your_server_invitation">Din server invitation</string>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
|
||||
<string name="use_conversations.im">Použiť conversations.im</string>
|
||||
<string name="use_chat.sum7.eu">Použiť chat.sum7.eu</string>
|
||||
<string name="create_new_account">Vytvoriť nové konto</string>
|
||||
<string name="do_you_have_an_account">Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá.</string>
|
||||
<string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im¹; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
|
||||
<string name="server_select_text">XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na chat.sum7.eu; poskytovateľ špeciálne vhodný na používanie s Conversations.</string>
|
||||
<string name="magic_create_text_on_x">Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
|
||||
<string name="magic_create_text_fixed">Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu.</string>
|
||||
<string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string>
|
||||
<string name="use_conversations.im">Sử dụng conversations.im</string>
|
||||
<string name="use_chat.sum7.eu">Sử dụng chat.sum7.eu</string>
|
||||
<string name="create_new_account">Tạo tài khoản mới</string>
|
||||
<string name="do_you_have_an_account">Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP.</string>
|
||||
<string name="server_select_text">XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im¹ được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations.</string>
|
||||
<string name="server_select_text">XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên chat.sum7.eu được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations.</string>
|
||||
<string name="magic_create_text_on_x">Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
|
||||
<string name="magic_create_text_fixed">Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn.</string>
|
||||
<string name="your_server_invitation">Lời mời vào máy chủ của bạn</string>
|
||||
|
|
|
@ -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,38 +35,39 @@ 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;
|
||||
}
|
||||
|
|
|
@ -408,6 +408,7 @@ public class Resolver {
|
|||
Result result = new Result();
|
||||
result.timeRequested = System.currentTimeMillis();
|
||||
result.port = port;
|
||||
result.directTls = useDirectTls(port);
|
||||
result.hostname = hostname;
|
||||
result.ip = ip;
|
||||
return result;
|
||||
|
|
|
@ -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