Compare commits

..

No commits in common. "develop" and "2.10.2-sum7" have entirely different histories.

29 changed files with 503 additions and 916 deletions

View File

@ -76,7 +76,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:4.9.2" implementation "com.squareup.okhttp3:okhttp:4.9.2"
implementation 'com.google.guava:guava:30.1.1-android' implementation 'com.google.guava:guava:30.1.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
// implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') // implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.32006' implementation 'org.webrtc:google-webrtc:1.0.32006'
} }
@ -93,7 +93,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 4202301 versionCode 42023
versionName "2.10.2" versionName "2.10.2"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations" applicationId "eu.sum7.conversations"
@ -262,4 +262,16 @@ android {
exclude 'META-INF/BCKEY.DSA' exclude 'META-INF/BCKEY.DSA'
exclude 'META-INF/BCKEY.SF' 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
}
}
}
} }

View File

@ -1 +0,0 @@
• Fix usage directTLS of manuelle enter an address

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string> <string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
<string name="use_chat.sum7.eu">chat.sum7.eu ব্যবহার করা যাক</string> <string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
<string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string> <string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
<string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string> <string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
<string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই chat.sum7.eu -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string> <string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
<string name="magic_create_text_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</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="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string> <string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন</string> <string name="if_contact_is_nearby_use_qr">পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন</string>
<string name="easy_invite_share_text">%1$sতে এসো, আর আমার সাথে কথা বলো: %2$s</string> <string name="easy_invite_share_text">%1$sতে এসো, আর আমার সাথে কথা বলো: %2$s</string>
<string name="share_invite_with">একটি আমন্ত্রণপত্র দেওয়া যাক...</string> <string name="share_invite_with">একটি আমন্ত্রণপত্র দেওয়া যাক...</string>
</resources> </resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pick_a_server">Vælg din XMPP-udbyder</string> <string name="pick_a_server">Vælg din XMPP-udbyder</string>
<string name="use_chat.sum7.eu">Brug chat.sum7.eu</string> <string name="use_conversations.im">Brug conversations.im</string>
<string name="create_new_account">Opret ny konto</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="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å chat.sum7.eu; 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å conversations.im¹; 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_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="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> <string name="your_server_invitation">Din server invitation</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation.</string> <string name="if_contact_is_nearby_use_qr">Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation.</string>
<string name="easy_invite_share_text">Deltag med %1$s og chat med mig: %2$s</string> <string name="easy_invite_share_text">Deltag med %1$s og chat med mig: %2$s</string>
<string name="share_invite_with">Del invitation med...</string> <string name="share_invite_with">Del invitation med...</string>
</resources> </resources>

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string> <string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
<string name="use_chat.sum7.eu">Použiť chat.sum7.eu</string> <string name="use_conversations.im">Použiť conversations.im</string>
<string name="create_new_account">Vytvoriť nové konto</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="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 chat.sum7.eu; 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 conversations.im¹; 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_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="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> <string name="tap_share_button_send_invite">Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu.</string>
<string name="if_contact_is_nearby_use_qr">Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie.</string> <string name="if_contact_is_nearby_use_qr">Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie.</string>
<string name="easy_invite_share_text">Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s</string> <string name="easy_invite_share_text">Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s</string>
<string name="share_invite_with">Zdieľať pozvánku s...</string> <string name="share_invite_with">Zdieľať pozvánku s...</string>
</resources> </resources>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string> <string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string>
<string name="use_chat.sum7.eu">Sử dụng chat.sum7.eu</string> <string name="use_conversations.im">Sử dụng conversations.im</string>
<string name="create_new_account">Tạo tài khoản mới</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="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 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="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="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_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="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> <string name="your_server_invitation">Lời mời vào máy chủ của bạn</string>
@ -13,4 +13,4 @@
<string name="if_contact_is_nearby_use_qr">Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn.</string> <string name="if_contact_is_nearby_use_qr">Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn.</string>
<string name="easy_invite_share_text">Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s</string> <string name="easy_invite_share_text">Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s</string>
<string name="share_invite_with">Chia sẻ lời mời với...</string> <string name="share_invite_with">Chia sẻ lời mời với...</string>
</resources> </resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,13 +13,10 @@ import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import com.google.common.math.DoubleMath;
import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import java.math.RoundingMode;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityShareLocationBinding; import eu.siacs.conversations.databinding.ActivityShareLocationBinding;
@ -31,213 +28,213 @@ import eu.siacs.conversations.utils.ThemeHelper;
public class ShareLocationActivity extends LocationActivity implements LocationListener { public class ShareLocationActivity extends LocationActivity implements LocationListener {
private Snackbar snackBar; private Snackbar snackBar;
private ActivityShareLocationBinding binding; private ActivityShareLocationBinding binding;
private boolean marker_fixed_to_loc = false; private boolean marker_fixed_to_loc = false;
private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
private Boolean noAskAgain = false; private Boolean noAskAgain = false;
@Override @Override
protected void onSaveInstanceState(@NonNull final Bundle outState) { protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(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 @Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState); super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
} }
} }
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location); this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location);
setSupportActionBar(binding.toolbar); setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar()); configureActionBar(getSupportActionBar());
setupMapView(binding.map, LocationProvider.getGeoPoint(this)); setupMapView(binding.map, LocationProvider.getGeoPoint(this));
this.binding.cancelButton.setOnClickListener(view -> { this.binding.cancelButton.setOnClickListener(view -> {
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);
finish(); finish();
}); });
this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE); this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
this.snackBar.setAction(R.string.enable, view -> { this.snackBar.setAction(R.string.enable, view -> {
if (isLocationEnabledAndAllowed()) { if (isLocationEnabledAndAllowed()) {
updateUi(); updateUi();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
} else if (!isLocationEnabled()) { } else if (!isLocationEnabled()) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} }
}); });
ThemeHelper.fix(this.snackBar); ThemeHelper.fix(this.snackBar);
this.binding.shareButton.setOnClickListener(this::shareLocation); this.binding.shareButton.setOnClickListener(view -> {
final Intent result = new Intent();
this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); 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.binding.fab.setOnClickListener(view -> { setResult(RESULT_OK, result);
if (!marker_fixed_to_loc) { finish();
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();
});
}
private void shareLocation(final View view) { this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
final Intent result = new Intent();
if (marker_fixed_to_loc && myLoc != null) {
result.putExtra("latitude", myLoc.getLatitude());
result.putExtra("longitude", myLoc.getLongitude());
result.putExtra("altitude", myLoc.getAltitude());
result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP));
} else {
final IGeoPoint markerPoint = this.binding.map.getMapCenter();
result.putExtra("latitude", markerPoint.getLatitude());
result.putExtra("longitude", markerPoint.getLongitude());
}
setResult(RESULT_OK, result);
finish();
}
@Override this.binding.fab.setOnClickListener(view -> {
public void onRequestPermissionsResult(final int requestCode, if (!marker_fixed_to_loc) {
@NonNull final String[] permissions, if (!isLocationEnabled()) {
@NonNull final int[] grantResults) { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
super.onRequestPermissionsResult(requestCode, permissions, grantResults); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(REQUEST_CODE_FAB_PRESSED);
}
}
toggleFixedLocation();
});
}
if (grantResults.length > 0 && @Override
grantResults[0] != PackageManager.PERMISSION_GRANTED && public void onRequestPermissionsResult(final int requestCode,
Build.VERSION.SDK_INT >= 23 && @NonNull final String[] permissions,
permissions.length > 0 && @NonNull final int[] grantResults) {
( super.onRequestPermissionsResult(requestCode, permissions, grantResults);
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()) { if (grantResults.length > 0 &&
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); grantResults[0] != PackageManager.PERMISSION_GRANTED &&
} Build.VERSION.SDK_INT >= 23 &&
updateUi(); 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;
}
@Override if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
protected void gotoLoc(final boolean setZoomLevel) { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
if (this.myLoc != null && mapController != null) { }
if (setZoomLevel) { updateUi();
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); }
}
mapController.animateTo(new GeoPoint(this.myLoc));
}
}
@Override @Override
protected void setMyLoc(final Location location) { protected void gotoLoc(final boolean setZoomLevel) {
this.myLoc = location; if (this.myLoc != null && mapController != null) {
} if (setZoomLevel) {
mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
}
mapController.animateTo(new GeoPoint(this.myLoc));
}
}
@Override @Override
protected void onPause() { protected void setMyLoc(final Location location) {
super.onPause(); this.myLoc = location;
} }
@Override @Override
protected void updateLocationMarkers() { protected void onPause() {
super.updateLocationMarkers(); super.onPause();
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 @Override
public void onLocationChanged(final Location location) { protected void updateLocationMarkers() {
if (this.myLoc == null) { super.updateLocationMarkers();
this.marker_fixed_to_loc = true; if (this.myLoc != null) {
} this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
updateUi(); if (this.marker_fixed_to_loc) {
if (LocationHelper.isBetterLocation(location, this.myLoc)) { this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
final Location oldLoc = this.myLoc; } else {
this.myLoc = location; this.binding.map.getOverlays().add(new Marker(marker_icon));
}
} else {
this.binding.map.getOverlays().add(new Marker(marker_icon));
}
}
// Don't jump back to the users location if they're not moving (more or less). @Override
if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { public void onLocationChanged(final Location location) {
gotoLoc(); 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;
updateLocationMarkers(); // 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();
}
@Override updateLocationMarkers();
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 void toggleFixedLocation() { private boolean isLocationEnabledAndAllowed() {
this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
if (this.marker_fixed_to_loc) { }
gotoLoc(false);
}
updateLocationMarkers();
updateUi();
}
@Override private void toggleFixedLocation() {
protected void updateUi() { this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { if (this.marker_fixed_to_loc) {
this.snackBar.dismiss(); gotoLoc(false);
} else { }
this.snackBar.show(); updateLocationMarkers();
} updateUi();
}
if (isLocationEnabledAndAllowed()) { @Override
this.binding.fab.setVisibility(View.VISIBLE); protected void updateUi() {
runOnUiThread(() -> { if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : this.snackBar.dismiss();
R.drawable.ic_gps_not_fixed_white_24dp); } else {
this.binding.fab.setContentDescription(getResources().getString( this.snackBar.show();
marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location }
));
this.binding.fab.invalidate(); if (isLocationEnabledAndAllowed()) {
}); this.binding.fab.setVisibility(View.VISIBLE);
} else { runOnUiThread(() -> {
this.binding.fab.setVisibility(View.GONE); 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);
}
}
} }

View File

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

View File

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

View File

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

View File

@ -408,7 +408,6 @@ public class Resolver {
Result result = new Result(); Result result = new Result();
result.timeRequested = System.currentTimeMillis(); result.timeRequested = System.currentTimeMillis();
result.port = port; result.port = port;
result.directTls = useDirectTls(port);
result.hostname = hostname; result.hostname = hostname;
result.ip = ip; result.ip = ip;
return result; return result;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -7,7 +8,6 @@ import androidx.annotation.Nullable;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
@ -25,15 +25,13 @@ import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
import java.util.ArrayDeque;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -141,7 +139,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>> pendingIceCandidates = new LinkedList<>(); private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>();
private final OmemoVerification omemoVerification = new OmemoVerification(); private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message; private final Message message;
private State state = State.NULL; private State state = State.NULL;
@ -149,9 +147,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private Set<Media> proposedMedia; private Set<Media> proposedMedia;
private RtpContentMap initiatorRtpContentMap; private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap; private RtpContentMap responderRtpContentMap;
private IceUdpTransportInfo.Setup peerDtlsSetup; private long rtpConnectionStarted = 0; //time of 'connected'
private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); private long rtpConnectionEnded = 0;
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
private ScheduledFuture<?> ringingTimeoutFuture; private ScheduledFuture<?> ringingTimeoutFuture;
JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
@ -193,6 +190,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
synchronized void deliverPacket(final JinglePacket jinglePacket) { synchronized void deliverPacket(final JinglePacket jinglePacket) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection");
switch (jinglePacket.getAction()) { switch (jinglePacket.getAction()) {
case SESSION_INITIATE: case SESSION_INITIATE:
receiveSessionInitiate(jinglePacket); receiveSessionInitiate(jinglePacket);
@ -253,15 +251,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveTransportInfo(final JinglePacket jinglePacket) { private void receiveTransportInfo(final JinglePacket jinglePacket) {
//Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received
if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
final RtpContentMap contentMap; final RtpContentMap contentMap;
try { try {
contentMap = RtpContentMap.of(jinglePacket); contentMap = RtpContentMap.of(jinglePacket);
} catch (final IllegalArgumentException | NullPointerException e) { } catch (IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
respondOk(jinglePacket);
return; return;
} }
receiveTransportInfo(jinglePacket, contentMap); 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);
}
} else { } else {
if (isTerminated()) { if (isTerminated()) {
respondOk(jinglePacket); respondOk(jinglePacket);
@ -273,161 +280,37 @@ 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) { private void processCandidates(final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) { final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
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 Group originalGroup = rtpContentMap.group;
final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); final List<String> identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags();
if (identificationTags.size() == 0) { 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"); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices");
} }
return identificationTags; 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);
}
}
} }
private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { private ListenableFuture<RtpContentMap> receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
@ -487,7 +370,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) {
try { try {
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(true); contentMap.requireDTLSFingerprint();
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e));
respondOk(jinglePacket); respondOk(jinglePacket);
@ -515,7 +398,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket); respondOk(jinglePacket);
pendingIceCandidates.addAll(contentMap.contents.entrySet());
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
if (candidates.size() > 0) {
pendingIceCandidates.push(candidates);
}
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
sendSessionAccept(); sendSessionAccept();
@ -584,7 +471,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void receiveSessionAccept(final RtpContentMap contentMap) { private void receiveSessionAccept(final RtpContentMap contentMap) {
this.responderRtpContentMap = contentMap; this.responderRtpContentMap = contentMap;
this.storePeerDtlsSetup(contentMap.getDtlsSetup());
final SessionDescription sessionDescription; final SessionDescription sessionDescription;
try { try {
sessionDescription = SessionDescription.of(contentMap); sessionDescription = SessionDescription.of(contentMap);
@ -603,10 +489,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} catch (final Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e));
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); sendSessionTerminate(Reason.FAILED_APPLICATION);
return; return;
} }
processCandidates(contentMap.contents.entrySet()); final List<String> identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags();
processCandidates(identificationTags, contentMap.contents.entrySet());
} }
private void sendSessionAccept() { private void sendSessionAccept() {
@ -650,7 +537,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try { try {
this.webRTCWrapper.setRemoteDescription(sdp).get(); this.webRTCWrapper.setRemoteDescription(sdp).get();
addIceCandidatesFromBlackLog(); addIceCandidatesFromBlackLog();
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
prepareSessionAccept(webRTCSessionDescription); prepareSessionAccept(webRTCSessionDescription);
} catch (final Exception e) { } catch (final Exception e) {
failureToAcceptSession(e); failureToAcceptSession(e);
@ -661,17 +548,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (isTerminated()) { if (isTerminated()) {
return; return;
} }
final Throwable rootCause = Throwables.getRootCause(throwable); Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(throwable));
Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); sendSessionTerminate(Reason.ofThrowable(throwable));
} }
private void addIceCandidatesFromBlackLog() { private void addIceCandidatesFromBlackLog() {
Map.Entry<String, RtpContentMap.DescriptionTransport> foo; while (!this.pendingIceCandidates.isEmpty()) {
while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidates(this.pendingIceCandidates.poll());
processCandidate(foo); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log");
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log");
} }
} }
@ -679,14 +564,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
this.responderRtpContentMap = respondingRtpContentMap; this.responderRtpContentMap = respondingRtpContentMap;
storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip());
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap);
Futures.addCallback(outgoingContentMapFuture, Futures.addCallback(outgoingContentMapFuture,
new FutureCallback<RtpContentMap>() { new FutureCallback<RtpContentMap>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionAccept(outgoingContentMap); sendSessionAccept(outgoingContentMap, webRTCSessionDescription);
} }
@Override @Override
@ -698,7 +581,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
); );
} }
private void sendSessionAccept(final RtpContentMap rtpContentMap) { private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription) {
if (isTerminated()) { if (isTerminated()) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do.");
return; return;
@ -706,6 +589,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
transitionOrThrow(State.SESSION_ACCEPTED); transitionOrThrow(State.SESSION_ACCEPTED);
final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
send(sessionAccept); send(sessionAccept);
try {
webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) {
failureToAcceptSession(e);
}
} }
private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) {
@ -953,10 +841,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return; return;
} }
try { try {
org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
prepareSessionInitiate(webRTCSessionDescription, targetState); prepareSessionInitiate(webRTCSessionDescription, targetState);
} catch (final Exception e) { } catch (final Exception e) {
//TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions
failureToInitiateSession(e, targetState); failureToInitiateSession(e, targetState);
} }
} }
@ -986,12 +873,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
this.initiatorRtpContentMap = rtpContentMap; this.initiatorRtpContentMap = rtpContentMap;
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap);
Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() { Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() {
@Override @Override
public void onSuccess(final RtpContentMap outgoingContentMap) { public void onSuccess(final RtpContentMap outgoingContentMap) {
sendSessionInitiate(outgoingContentMap, targetState); sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState);
} }
@Override @Override
@ -1001,7 +887,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
if (isTerminated()) { if (isTerminated()) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do.");
return; return;
@ -1009,6 +895,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.transitionOrThrow(targetState); this.transitionOrThrow(targetState);
final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
send(sessionInitiate); send(sessionInitiate);
try {
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) {
failureToInitiateSession(e, targetState);
}
} }
private ListenableFuture<RtpContentMap> encryptSessionInitiate(final RtpContentMap rtpContentMap) { private ListenableFuture<RtpContentMap> encryptSessionInitiate(final RtpContentMap rtpContentMap) {
@ -1074,48 +965,36 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private synchronized void handleIqResponse(final Account account, final IqPacket response) { private synchronized void handleIqResponse(final Account account, final IqPacket response) {
if (response.getType() == IqPacket.TYPE.ERROR) { if (response.getType() == IqPacket.TYPE.ERROR) {
handleIqErrorResponse(response); final String errorCondition = response.getErrorCondition();
return; 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();
} }
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) { private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
@ -1126,16 +1005,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.finish(); this.finish();
} }
private void respondWithTieBreak(final JinglePacket jinglePacket) {
respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
}
private void respondWithOutOfOrder(final JinglePacket jinglePacket) { private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait");
}
void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) {
jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType);
} }
private void respondOk(final JinglePacket jinglePacket) { private void respondOk(final JinglePacket jinglePacket) {
@ -1172,7 +1043,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.CONNECTING; return RtpEndUserState.CONNECTING;
} }
case SESSION_ACCEPTED: case SESSION_ACCEPTED:
return getPeerConnectionStateAsEndUserState(); //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;
}
case REJECTED: case REJECTED:
case REJECTED_RACED: case REJECTED_RACED:
case TERMINATED_DECLINED_OR_BUSY: case TERMINATED_DECLINED_OR_BUSY:
@ -1193,7 +1081,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return RtpEndUserState.RETRACTED; return RtpEndUserState.RETRACTED;
} }
case TERMINATED_CONNECTIVITY_ERROR: case TERMINATED_CONNECTIVITY_ERROR:
return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
case TERMINATED_APPLICATION_FAILURE: case TERMINATED_APPLICATION_FAILURE:
return RtpEndUserState.APPLICATION_ERROR; return RtpEndUserState.APPLICATION_ERROR;
case TERMINATED_SECURITY_ERROR: case TERMINATED_SECURITY_ERROR:
@ -1202,29 +1090,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state));
} }
private RtpEndUserState getPeerConnectionStateAsEndUserState() {
final PeerConnection.PeerConnectionState state;
try {
state = webRTCWrapper.getState();
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
//We usually close the WebRTCWrapper *before* transitioning so we might still
//be in SESSION_ACCEPTED even though the peerConnection has been torn down
return RtpEndUserState.ENDING_CALL;
}
switch (state) {
case CONNECTED:
return RtpEndUserState.CONNECTED;
case NEW:
case CONNECTING:
return RtpEndUserState.CONNECTING;
case CLOSED:
return RtpEndUserState.ENDING_CALL;
default:
return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING;
}
}
public Set<Media> getMedia() { public Set<Media> getMedia() {
final State current = getState(); final State current = getState();
if (current == State.NULL) { if (current == State.NULL) {
@ -1467,13 +1332,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
public void onIceCandidate(final IceCandidate iceCandidate) { public void onIceCandidate(final IceCandidate iceCandidate) {
final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp);
final String ufrag = rtpContentMap.getCredentials().ufrag;
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag);
if (candidate == null) {
Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString());
return;
}
Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString());
sendTransportInfo(iceCandidate.sdpMid, candidate); sendTransportInfo(iceCandidate.sdpMid, candidate);
} }
@ -1481,97 +1340,30 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
@Override @Override
public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState);
this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) {
if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.rtpConnectionStarted = SystemClock.elapsedRealtime();
this.sessionDuration.start();
updateOngoingCallNotification();
} else if (this.sessionDuration.isRunning()) {
this.sessionDuration.stop();
updateOngoingCallNotification();
} }
if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) {
final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); this.rtpConnectionEnded = SystemClock.elapsedRealtime();
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();
}
} }
updateEndUserState(); //TODO 'failed' means we need to restart ICE
} //
//TODO 'disconnected' can probably be ignored as "This is a less stringent test than failed
@Override // and may trigger intermittently and resolve just as spontaneously on less reliable networks,
public void onRenegotiationNeeded() { // or during temporary disconnections. When the problem resolves, the connection may return
this.webRTCWrapper.execute(this::initiateIceRestart); // 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)) {
private void initiateIceRestart() { if (isTerminated()) {
//TODO discover new TURN/STUN credentials Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state);
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; return;
} }
if (response.getType() == IqPacket.TYPE.ERROR) { new Thread(this::closeWebRTCSessionAfterFailedConnection).start();
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 { } else {
this.responderRtpContentMap = rtpContentMap; updateEndUserState();
} }
} }
private void setRemoteContentMap(final RtpContentMap rtpContentMap) {
if (isInitiator()) {
this.responderRtpContentMap = rtpContentMap;
} else {
this.initiatorRtpContentMap = rtpContentMap;
}
}
private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException {
final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get();
return SessionDescription.parse(sessionDescription.description);
}
private void closeWebRTCSessionAfterFailedConnection() { private void closeWebRTCSessionAfterFailedConnection() {
this.webRTCWrapper.close(); this.webRTCWrapper.close();
synchronized (this) { synchronized (this) {
@ -1583,12 +1375,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} }
public boolean zeroDuration() { public long getRtpConnectionStarted() {
return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0; return this.rtpConnectionStarted;
} }
public long getCallDuration() { public long getRtpConnectionEnded() {
return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); return this.rtpConnectionEnded;
} }
public AppRTCAudioManager getAudioManager() { public AppRTCAudioManager getAudioManager() {
@ -1635,15 +1427,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private void updateOngoingCallNotification() { private void updateOngoingCallNotification() {
final State state = this.state; if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) {
if (STATES_SHOWING_ONGOING_CALL.contains(state)) { xmppConnectionService.setOngoingCall(id, getMedia());
final boolean reconnecting;
if (state == State.SESSION_ACCEPTED) {
reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING;
} else {
reconnecting = false;
}
xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting);
} else { } else {
xmppConnectionService.removeOngoingCall(); xmppConnectionService.removeOngoingCall();
} }
@ -1722,7 +1507,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private void writeLogMessage(final State state) { private void writeLogMessage(final State state) {
final long duration = getCallDuration(); final long started = this.rtpConnectionStarted;
long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started;
if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) {
writeLogMessageSuccess(duration); writeLogMessageSuccess(duration);
} else { } else {
@ -1767,6 +1553,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return webRTCWrapper.getRemoteVideoTrack(); return webRTCWrapper.getRemoteVideoTrack();
} }
public EglBase.Context getEglBaseContext() { public EglBase.Context getEglBaseContext() {
return webRTCWrapper.getEglBaseContext(); return webRTCWrapper.getEglBaseContext();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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