Compare commits

..

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

41 changed files with 605 additions and 1175 deletions

View File

@ -1,10 +1,5 @@
# Changelog
### Version 2.10.2
* Fix crash when rendering some quotes
* Fix crash in welcome screen
### Version 2.10.1
* Fix issue with some videos not being compressed

View File

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.android.tools.build:gradle:7.0.2'
}
}
@ -64,19 +64,18 @@ dependencies {
implementation "com.wefika:flowlayout:0.4.1"
implementation 'com.otaliastudios:transcoder:0.10.4'
implementation 'org.jxmpp:jxmpp-jid:1.0.2'
implementation 'org.jxmpp:jxmpp-jid:1.0.1'
implementation 'org.osmdroid:osmdroid-android:6.1.10'
implementation 'org.hsluv:hsluv:0.2'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'me.drakeet.support:toastcompat:1.1.0'
implementation "com.leinardi.android:speed-dial:3.2.0"
implementation "com.leinardi.android:speed-dial:2.0.1"
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.okhttp3:okhttp:4.9.2"
implementation "com.squareup.okhttp3:okhttp:4.9.1"
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 'org.webrtc:google-webrtc:1.0.32006'
}
@ -93,8 +92,8 @@ android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 4202301
versionName "2.10.2"
versionCode 42022
versionName "2.10.1"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
@ -262,4 +261,14 @@ android {
exclude 'META-INF/BCKEY.DSA'
exclude 'META-INF/BCKEY.SF'
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
if (baseAbiVersionCode != null) {
output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode
}
}
}
}

View File

@ -1,2 +0,0 @@
• Fix crash when rendering some quotes
• Fix crash in welcome screen

View File

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

View File

@ -106,8 +106,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
}
@Override
public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
public void onNewIntent(Intent intent) {
if (intent != null) {
setIntent(intent);
}

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="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_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
<string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Vælg din XMPP-udbyder</string>
<string name="use_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="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_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>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Vyberte si svojho XMPP poskytovateľa</string>
<string name="use_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="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_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>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Chọn nhà cung cấp XMPP của bạn</string>
<string name="use_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="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_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>

View File

@ -23,13 +23,13 @@ public class JabberIdContact extends AbstractPhoneContact {
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA
};
private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))";
private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and " + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + "=?))";
private static final String[] SELECTION_ARGS = {
ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE,
String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER),
String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM),
"xmpp"
"XMPP"
};
private final Jid jid;

View File

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

View File

@ -572,8 +572,8 @@ public class XmppConnectionService extends Service {
}
}
public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
final String compressPictures = getCompressPicturesPreference();
if ("never".equals(compressPictures)
@ -1298,8 +1298,8 @@ public class XmppConnectionService extends Service {
toggleForegroundService(false);
}
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
ongoingCall.set(new OngoingCall(id, media, reconnecting));
public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
ongoingCall.set(new OngoingCall(id, media));
toggleForegroundService(false);
}
@ -1315,7 +1315,7 @@ public class XmppConnectionService extends Service {
final Notification notification;
final int id;
if (ongoing != null) {
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media);
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
startForeground(id, notification);
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
@ -4869,14 +4869,12 @@ public class XmppConnectionService extends Service {
}
public static class OngoingCall {
public final AbstractJingleConnection.Id id;
public final Set<Media> media;
public final boolean reconnecting;
private final AbstractJingleConnection.Id id;
private final Set<Media> media;
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media) {
this.id = id;
this.media = media;
this.reconnecting = reconnecting;
}
@Override
@ -4884,12 +4882,12 @@ public class XmppConnectionService extends Service {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
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
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();
}
private void attachImageToConversation(Conversation conversation, Uri uri, String type) {
private void attachImageToConversation(Conversation conversation, Uri uri) {
if (conversation == null) {
return;
}
final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG);
prepareFileToast.show();
activity.delegateUriPermissionsToService(uri);
activity.xmppConnectionService.attachImageToConversation(conversation, uri, type,
activity.xmppConnectionService.attachImageToConversation(conversation, uri,
new UiCallback<Message>() {
@Override
@ -856,15 +856,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
toggleInputMethod();
break;
case ATTACHMENT_CHOICE_LOCATION:
final double latitude = data.getDoubleExtra("latitude", 0);
final double longitude = data.getDoubleExtra("longitude", 0);
final int accuracy = data.getIntExtra("accuracy", 0);
final Uri geo;
if (accuracy > 0) {
geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy));
} else {
geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude));
}
double latitude = data.getDoubleExtra("latitude", 0);
double longitude = data.getDoubleExtra("longitude", 0);
Uri geo = Uri.parse("geo:" + latitude + "," + longitude);
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION));
toggleInputMethod();
break;
@ -895,7 +889,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
attachLocationToConversation(conversation, attachment.getUri());
} else if (attachment.getType() == Attachment.Type.IMAGE) {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE");
attachImageToConversation(conversation, attachment.getUri(), attachment.getMime());
attachImageToConversation(conversation, attachment.getUri());
} else {
Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO");
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 pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
final String type = extras.getString(ConversationsActivity.EXTRA_TYPE);
final List<Uri> uris = extractUris(extras);
if (uris != null && uris.size() > 0) {
if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) {
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION));
} else {
final List<Uri> cleanedUris = cleanUris(new ArrayList<>(uris));
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type));
mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris));
}
toggleInputMethod();
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_POST_INIT_ACTION = "post_init_action";
public static final String POST_ACTION_RECORD_VOICE = "record_voice";
public static final String EXTRA_TYPE = "type";
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
ACTION_VIEW_CONVERSATION,

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

View File

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

View File

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

View File

@ -37,14 +37,11 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -54,8 +51,6 @@ import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.textfield.TextInputLayout;
import com.leinardi.android.speeddial.SpeedDialActionItem;
import com.leinardi.android.speeddial.SpeedDialView;
import java.util.ArrayList;
import java.util.Collections;
@ -271,7 +266,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
binding.speedDial.inflate(R.menu.start_conversation_fab_submenu);
binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
@ -342,21 +338,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
});
}
private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
speedDialView.clearActionItems();
final PopupMenu popupMenu = new PopupMenu(this, new View(this));
popupMenu.inflate(menuRes);
final Menu menu = popupMenu.getMenu();
for (int i = 0; i < menu.size(); i++) {
final MenuItem menuItem = menu.getItem(i);
final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
.setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
.setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
.create();
speedDialView.addActionItem(actionItem);
}
}
public static boolean isValidJid(String input) {
try {
Jid jid = Jid.ofEscaped(input);

View File

@ -7,39 +7,24 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.common.base.Strings;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.ProvisioningUtils;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
public class UriHandlerActivity extends AppCompatActivity {
@ -49,9 +34,7 @@ public class UriHandlerActivity extends AppCompatActivity {
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790;
private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>");
private ActivityUriHandlerBinding binding;
private Call call;
private boolean handled = false;
public static void scan(final Activity activity) {
scan(activity, false);
@ -94,7 +77,9 @@ public class UriHandlerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled", false);
getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content));
setSupportActionBar(findViewById(R.id.toolbar));
}
@Override
@ -103,17 +88,23 @@ public class UriHandlerActivity extends AppCompatActivity {
handleIntent(getIntent());
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean("handled", this.handled);
super.onSaveInstanceState(savedInstanceState);
}
@Override
public void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private boolean handleUri(final Uri uri) {
return handleUri(uri, false);
private void handleUri(Uri uri) {
handleUri(uri, false);
}
private boolean handleUri(final Uri uri, final boolean scanned) {
private void handleUri(Uri uri, final boolean scanned) {
final Intent intent;
final XmppUri xmppUri = new XmppUri(uri);
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
@ -123,22 +114,19 @@ public class UriHandlerActivity extends AppCompatActivity {
final Jid jid = xmppUri.getJid();
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
showError(R.string.account_already_exists);
return false;
Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show();
return;
}
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
startActivity(intent);
return true;
return;
}
if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
return true;
return;
}
} else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
showError(R.string.account_registrations_are_not_supported);
return false;
}
if (accounts.size() == 0) {
@ -146,14 +134,15 @@ public class UriHandlerActivity extends AppCompatActivity {
intent = SignupUtils.getSignUpIntent(this);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
return true;
} else {
showError(R.string.invalid_jid);
return false;
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
}
return;
}
if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) {
final Jid jid = xmppUri.getJid();
final String body = xmppUri.getBody();
@ -188,57 +177,11 @@ public class UriHandlerActivity extends AppCompatActivity {
intent.putExtra("scanned", scanned);
intent.setData(uri);
} else {
showError(R.string.invalid_jid);
return false;
}
startActivity(intent);
return true;
}
private void checkForLinkHeader(final HttpUrl url) {
Log.d(Config.LOGTAG, "checking for link header on " + url);
this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder()
.url(url)
.head()
.build());
this.call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d(Config.LOGTAG, "unable to check HTTP url", e);
showError(R.string.no_xmpp_adddress_found);
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
if (response.isSuccessful()) {
final String linkHeader = response.header("Link");
if (linkHeader != null && processLinkHeader(linkHeader)) {
Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
return;
}
}
showError(R.string.no_xmpp_adddress_found);
}
});
}
private boolean processLinkHeader(final String header) {
final Matcher matcher = LINK_HEADER_PATTERN.matcher(header);
if (matcher.find()) {
final String group = matcher.group();
final String link = group.substring(1, group.length() - 1);
if (handleUri(Uri.parse(link))) {
finish();
return true;
}
}
return false;
}
private void showError(@StringRes int error) {
this.binding.progress.setVisibility(View.INVISIBLE);
this.binding.error.setText(error);
this.binding.error.setVisibility(View.VISIBLE);
startActivity(intent);
}
private static Class<?> findShareViaAccountClass() {
@ -249,33 +192,29 @@ public class UriHandlerActivity extends AppCompatActivity {
}
}
private void handleIntent(final Intent data) {
final String action = data == null ? null : data.getAction();
if (action == null) {
private void handleIntent(Intent data) {
if (handled) {
return;
}
switch (action) {
case Intent.ACTION_MAIN:
binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
break;
case Intent.ACTION_VIEW:
case Intent.ACTION_SENDTO:
if (handleUri(data.getData())) {
if (data == null || data.getAction() == null) {
finish();
}
break;
case ACTION_SCAN_QR_CODE:
Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning());
setIntent(createMainIntent());
startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE);
break;
}
return;
}
private Intent createMainIntent() {
final Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning());
return intent;
handled = true;
switch (data.getAction()) {
case Intent.ACTION_VIEW:
case Intent.ACTION_SENDTO:
handleUri(data.getData());
break;
case ACTION_SCAN_QR_CODE:
Intent intent = new Intent(this, ScanActivity.class);
startActivityForResult(intent, REQUEST_SCAN_QR_CODE);
return;
}
finish();
}
private boolean allowProvisioning() {
@ -287,7 +226,6 @@ public class UriHandlerActivity extends AppCompatActivity {
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
super.onActivityResult(requestCode, requestCode, intent);
if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) {
final boolean allowProvisioning = allowProvisioning();
final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
if (Strings.isNullOrEmpty(result)) {
finish();
@ -296,34 +234,18 @@ public class UriHandlerActivity extends AppCompatActivity {
if (result.startsWith("BEGIN:VCARD\n")) {
final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
if (matcher.find()) {
if (handleUri(Uri.parse(matcher.group(2)), true)) {
handleUri(Uri.parse(matcher.group(2)), true);
}
finish();
}
} else {
showError(R.string.no_xmpp_adddress_found);
}
return;
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) {
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) {
ProvisioningUtils.provision(this, result);
finish();
return;
}
final Uri uri = Uri.parse(result.trim());
if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
if (httpUrl != null) {
checkForLinkHeader(httpUrl);
} else {
finish();
handleUri(Uri.parse(result), true);
}
} else if (handleUri(uri, true)) {
finish();
} else {
setIntent(new Intent(Intent.ACTION_VIEW, uri));
}
} else {
finish();
}
}
private static boolean looksLikeJsonObject(final String input) {

View File

@ -368,7 +368,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
if (
QuoteHelper.isPositionQuoteStart(body, i)
) {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;

View File

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

View File

@ -11,47 +11,47 @@ public class QuoteHelper {
public static final char QUOTE_ALT_CHAR = '»';
public static final char QUOTE_ALT_END_CHAR = '«';
public static boolean isPositionQuoteCharacter(CharSequence body, int pos) {
public static boolean isPositionQuoteCharacter(CharSequence body, int pos){
// second part of logical check actually goes against the logic indicated in the method name, since it also checks for context
// but it's very useful
return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos);
}
public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) {
public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos){
return body.charAt(pos) == QUOTE_END_CHAR;
}
public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) {
public static boolean isPositionAltQuoteCharacter (CharSequence body, int pos){
return body.charAt(pos) == QUOTE_ALT_CHAR;
}
public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) {
public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos){
return body.charAt(pos) == QUOTE_ALT_END_CHAR;
}
public static boolean isPositionAltQuoteStart(CharSequence body, int pos) {
public static boolean isPositionAltQuoteStart(CharSequence body, int pos){
return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos);
}
public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) {
return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1);
return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos +1 );
}
// 'Prequote' means anything we require or can accept in front of a QuoteChar
public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) {
public static boolean isPositionPrecededByPrequote(CharSequence body, int pos){
return UIHelper.isPositionPrecededByLineStart(body, pos);
}
public static boolean isPositionQuoteStart(CharSequence body, int pos) {
public static boolean isPositionQuoteStart (CharSequence body, int pos){
return (isPositionQuoteCharacter(body, pos)
&& isPositionPrecededByPreQuote(body, pos)
&& isPositionPrecededByPrequote(body, pos)
&& (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos)
|| isPositionFollowedByQuoteChar(body, pos)));
}
public static boolean bodyContainsQuoteStart(CharSequence body) {
for (int i = 0; i < body.length(); i++) {
if (isPositionQuoteStart(body, i)) {
public static boolean bodyContainsQuoteStart (CharSequence body){
for (int i = 0; i < body.length(); i++){
if (isPositionQuoteStart(body, i)){
return true;
}
}
@ -76,7 +76,7 @@ public class QuoteHelper {
return false;
}
public static boolean isNestedTooDeeply(CharSequence line) {
public static boolean isNestedTooDeeply (CharSequence line){
if (isPositionQuoteStart(line, 0)) {
int nestingDepth = 1;
for (int i = 1; i < line.length(); i++) {
@ -91,9 +91,9 @@ public class QuoteHelper {
return false;
}
public static String replaceAltQuoteCharsInText(String text) {
for (int i = 0; i < text.length(); i++) {
if (isPositionAltQuoteStart(text, i)) {
public static String replaceAltQuoteCharsInText(String text){
for (int i = 0; i < text.length(); i++){
if (isPositionAltQuoteStart(text, i)){
text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1);
}
}

View File

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

View File

@ -21,8 +21,6 @@ import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -276,8 +274,6 @@ public final class MimeUtils {
add("image/ico", "ico");
add("image/ief", "ief");
add("image/heic", "heic");
add("image/heif", "heif");
add("image/avif", "avif");
// add ".jpg" first so it will be the default for guessExtensionFromMimeType
add("image/jpeg", "jpg");
add("image/jpeg", "jpeg");
@ -591,19 +587,15 @@ public final class MimeUtils {
}
public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) {
if (Strings.isNullOrEmpty(path)) {
if (path == null || path.isEmpty()) {
return null;
}
final String filenameQueryAnchor = path.substring(path.lastIndexOf('/') + 1);
final String filenameQuery = cutBefore(filenameQueryAnchor, '#');
final String filename = cutBefore(filenameQuery, '?');
final int dotPosition = filename.lastIndexOf('.');
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
int dotPosition = filename.lastIndexOf(".");
if (dotPosition == -1) {
return null;
}
final String extension = filename.substring(dotPosition + 1);
if (dotPosition != -1) {
String extension = filename.substring(dotPosition + 1);
// we want the real file extension, not the crypto one
if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
return extractRelevantExtension(filename.substring(0, dotPosition));
@ -611,13 +603,6 @@ public final class MimeUtils {
return extension;
}
}
private static String cutBefore(final String input, final char c) {
final int position = input.indexOf(c);
if (position > 0) {
return input.substring(0, position);
} else {
return input;
}
return null;
}
}

View File

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

View File

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

View File

@ -35,8 +35,6 @@ public class XmppUri {
private Map<String, String> parameters = Collections.emptyMap();
private boolean safeSource = true;
public static final String INVITE_DOMAIN = "conversations.im";
public XmppUri(final String uri) {
try {
parse(Uri.parse(uri));
@ -138,10 +136,10 @@ public class XmppUri {
return;
}
this.uri = uri;
final String scheme = uri.getScheme();
final String host = uri.getHost();
String scheme = uri.getScheme();
String host = uri.getHost();
List<String> segments = uri.getPathSegments();
if ("https".equalsIgnoreCase(scheme) && INVITE_DOMAIN.equalsIgnoreCase(host)) {
if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) {
if (segments.size() >= 2 && segments.get(1).contains("@")) {
// sample : https://conversations.im/i/foo@bar.com
try {

View File

@ -1,7 +1,5 @@
package eu.siacs.conversations.xml;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
@ -167,9 +165,8 @@ public class Element {
return this.attributes;
}
@NotNull
public String toString() {
final StringBuilder elementOutput = new StringBuilder();
StringBuilder elementOutput = new StringBuilder();
if ((content == null) && (children.size() == 0)) {
Tag emptyTag = Tag.empty(name);
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 AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
public static final String JINGLE = "urn:xmpp:jingle:1";
public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1";
public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?colorPrimaryDark"
android:padding="24dp">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/white" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progress"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Conversations.Body2"
android:textColor="@color/white87"
android:visibility="invisible" />
</RelativeLayout>
</layout>

View File

@ -2,22 +2,22 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/discover_public_channels"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/discover_channels" />
android:title="@string/discover_channels"
android:icon="@drawable/ic_search_white_24dp"/>
<item
android:id="@+id/join_public_channel"
android:icon="@drawable/ic_input_white_24dp"
android:title="@string/join_public_channel" />
android:title="@string/join_public_channel"
android:icon="@drawable/ic_input_white_24dp"/>
<item
android:id="@+id/create_public_channel"
android:icon="@drawable/ic_public_white_24dp"
android:title="@string/create_public_channel" />
android:title="@string/create_public_channel"
android:icon="@drawable/ic_public_white_24dp"/>
<item
android:id="@+id/create_private_group_chat"
android:icon="@drawable/ic_group_white_24dp"
android:title="@string/create_private_group_chat" />
android:title="@string/create_private_group_chat"
android:icon="@drawable/ic_group_white_24dp"/>
<item
android:id="@+id/create_contact"
android:icon="@drawable/ic_person_white_48dp"
android:title="@string/add_contact" />
android:title="@string/add_contact"
android:icon="@drawable/ic_person_white_48dp"/>
</menu>

View File

@ -904,7 +904,6 @@
<string name="rtp_state_incoming_video_call">Incoming video call</string>
<string name="rtp_state_connecting">Connecting</string>
<string name="rtp_state_connected">Connected</string>
<string name="rtp_state_reconnecting">Reconnecting</string>
<string name="rtp_state_accepting_call">Accepting call</string>
<string name="rtp_state_ending_call">Ending call</string>
<string name="answer_call">Answer</string>
@ -920,8 +919,6 @@
<string name="hang_up">Hang up</string>
<string name="ongoing_call">Ongoing call</string>
<string name="ongoing_video_call">Ongoing video call</string>
<string name="reconnecting_call">Reconnecting call</string>
<string name="reconnecting_video_call">Reconnecting video call</string>
<string name="disable_tor_to_make_call">Disable Tor to make calls</string>
<string name="incoming_call">Incoming call</string>
<string name="incoming_call_duration">Incoming call · %s</string>
@ -971,7 +968,5 @@
<string name="backup_started_message">The backup has been started. Youll get a notification once it has been completed.</string>
<string name="unable_to_enable_video">Unable to enable video.</string>
<string name="plain_text_document">Plain text document</string>
<string name="account_registrations_are_not_supported">Account registrations are not supported</string>
<string name="no_xmpp_adddress_found">No XMPP address found</string>
</resources>