...
 
Commits (19)
# Changelog
### Version 2.8.8
* Fixed notifications not showing up under certain conditions
* Fixed compatibility issues and crashes related to A/V calls
### Version 2.8.7
* Show help button if A/V call fails
......
......@@ -119,7 +119,7 @@ images.each do |source_filename, settings|
else
path = "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png"
end
execute_cmd "#{inkscape} -f #{source_filename} -z -C -w #{width} -h #{height} -e #{path}"
execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -o #{path}"
top = []
right = []
......
......@@ -96,8 +96,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
versionCode 393
versionName "2.8.7"
versionCode 394
versionName "2.8.8"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
......
* Show help button if A/V call fails
* Fixed some annoying crashes
* Fixed Jingle connections (file transfer + calls) with bare JIDs
• Show help button if A/V call fails
• Fixed some annoying crashes
• Fixed Jingle connections (file transfer + calls) with bare JIDs
• Fixed notifications not showing up under certain conditions
• Fixed compatibility issues and crashes related to A/V calls
......@@ -38,6 +38,40 @@
<data android:mimeType="application/vnd.conversations.backup" />
<data android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ceb" />
<data android:pathPattern=".*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ceb" />
<data android:pathPattern=".*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
</intent-filter>
</activity>
</application>
</manifest>
package eu.siacs.conversations.entities;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import eu.siacs.conversations.xmpp.Jid;
public class AccountConfiguration {
private static final Gson GSON = new GsonBuilder().create();
public Protocol protocol;
public String address;
public String password;
public Jid getJid() {
return Jid.ofEscaped(address);
}
public static AccountConfiguration parse(final String input) {
final AccountConfiguration c;
try {
c = GSON.fromJson(input, AccountConfiguration.class);
} catch (JsonSyntaxException e) {
throw new IllegalArgumentException("Not a valid JSON string", e);
}
Preconditions.checkArgument(
c.protocol == Protocol.XMPP,
"Protocol must be XMPP"
);
Preconditions.checkArgument(
c.address != null && c.getJid().isBareJid() && !c.getJid().isDomainJid(),
"Invalid XMPP address"
);
Preconditions.checkArgument(
c.password != null && c.password.length() > 0,
"No password specified"
);
return c;
}
public enum Protocol {
@SerializedName("xmpp") XMPP,
}
}
......@@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
......@@ -25,6 +26,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityWelcomeBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.InstallReferrerUtils;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
......@@ -46,35 +48,37 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
activity.overridePendingTransition(0, 0);
}
public void onInstallReferrerDiscovered(final String referrer) {
public void onInstallReferrerDiscovered(final Uri referrer) {
Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer);
if (referrer != null) {
if ("xmpp".equalsIgnoreCase(referrer.getScheme())) {
final XmppUri xmppUri = new XmppUri(referrer);
runOnUiThread(() -> processXmppUri(xmppUri));
} else {
Log.i(Config.LOGTAG, "install referrer was not an XMPP uri");
}
}
private boolean processXmppUri(final XmppUri xmppUri) {
if (xmppUri.isValidJid()) {
final String preauth = xmppUri.getParameter("preauth");
final Jid jid = xmppUri.getJid();
final Intent intent;
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth);
} else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
} else {
intent = null;
}
if (intent != null) {
startActivity(intent);
finish();
return true;
}
this.inviteUri = xmppUri;
private void processXmppUri(final XmppUri xmppUri) {
if (!xmppUri.isValidJid()) {
return;
}
final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
final Jid jid = xmppUri.getJid();
final Intent intent;
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
} else if (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());
} else {
intent = null;
}
if (intent != null) {
startActivity(intent);
finish();
return;
}
return false;
this.inviteUri = xmppUri;
}
@Override
......@@ -143,10 +147,12 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.welcome_menu, menu);
final MenuItem scan = menu.findItem(R.id.action_scan_qr_code);
scan.setVisible(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA));
scan.setVisible(Compatibility.hasFeatureCamera(this));
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
......@@ -156,7 +162,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
}
break;
case R.id.action_scan_qr_code:
UriHandlerActivity.scan(this);
UriHandlerActivity.scan(this, true);
break;
case R.id.action_add_account_with_cert:
addAccountFromKey();
......@@ -183,7 +189,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
@Override
public void onAccountCreated(final Account account) {
final Intent intent = new Intent(this, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().asBareJid().toString());
intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
intent.putExtra("init", true);
addInviteUri(intent);
startActivity(intent);
......
package eu.siacs.conversations.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.AccountConfiguration;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.EditAccountActivity;
import eu.siacs.conversations.xmpp.Jid;
public class ProvisioningUtils {
public static void provision(final Activity activity, final String json) {
final AccountConfiguration accountConfiguration;
try {
accountConfiguration = AccountConfiguration.parse(json);
} catch (final IllegalArgumentException e) {
Toast.makeText(activity, R.string.improperly_formatted_provisioning, Toast.LENGTH_LONG).show();
return;
}
final Jid jid = accountConfiguration.getJid();
final List<Jid> accounts = DatabaseBackend.getInstance(activity).getAccountJids(true);
if (accounts.contains(jid)) {
Toast.makeText(activity, R.string.account_already_exists, Toast.LENGTH_LONG).show();
return;
}
final Intent serviceIntent = new Intent(activity, XmppConnectionService.class);
serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT);
serviceIntent.putExtra("address", jid.asBareJid().toEscapedString());
serviceIntent.putExtra("password", accountConfiguration.password);
Compatibility.startService(activity, serviceIntent);
final Intent intent = new Intent(activity, EditAccountActivity.class);
intent.putExtra("jid", jid.asBareJid().toEscapedString());
intent.putExtra("init", true);
activity.startActivity(intent);
}
}
......@@ -3,4 +3,6 @@
<string name="pick_a_server">Выберите своего XMPP-провайдера</string>
<string name="use_chat.sum7.eu">Использовать chat.sum7.eu</string>
<string name="create_new_account">Создать новый аккаунт</string>
</resources>
<string name="do_you_have_an_account">У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.\nНекоторые провайдеры электронной почты также регистрируют аккаунты XMPP. </string>
<string name="server_select_text">XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на chat.sum7.eu, сервер, специально предназначеный для работы с приложением Conv6sations.</string>
</resources>
......@@ -8,4 +8,5 @@
<string name="magic_create_text_on_x">You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address.</string>
<string name="magic_create_text_fixed">You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address.</string>
<string name="your_server_invitation">Your server invitation</string>
<string name="improperly_formatted_provisioning">Improperly formatted provisioning code</string>
</resources>
......@@ -2,6 +2,7 @@ package eu.siacs.conversations.utils;
import android.app.Activity;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
......@@ -9,6 +10,7 @@ import android.util.Log;
import com.android.installreferrer.api.InstallReferrerClient;
import com.android.installreferrer.api.InstallReferrerStateListener;
import com.android.installreferrer.api.ReferrerDetails;
import com.google.common.base.Strings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.ui.WelcomeActivity;
......@@ -49,8 +51,11 @@ public class InstallReferrerUtils implements InstallReferrerStateListener {
try {
final ReferrerDetails referrerDetails = installReferrerClient.getInstallReferrer();
final String referrer = referrerDetails.getInstallReferrer();
welcomeActivity.onInstallReferrerDiscovered(referrer);
} catch (final RemoteException e) {
if (Strings.isNullOrEmpty(referrer)) {
return;
}
welcomeActivity.onInstallReferrerDiscovered(Uri.parse(referrer));
} catch (final RemoteException | IllegalArgumentException e) {
Log.d(Config.LOGTAG, "unable to get install referrer", e);
}
} else {
......
......@@ -7,6 +7,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import org.json.JSONArray;
import org.json.JSONException;
......@@ -169,6 +170,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return first;
}
public String findMostRecentRemoteDisplayableId() {
final boolean multi = mode == Conversation.MODE_MULTI;
synchronized (this.messages) {
for(final Message message : Lists.reverse(this.messages)) {
if (message.getStatus() == Message.STATUS_RECEIVED) {
final String serverMsgId = message.getServerMsgId();
if (serverMsgId != null && multi) {
return serverMsgId;
}
return message.getRemoteMsgId();
}
}
}
return null;
}
public Message findUnsentMessageWithUuid(String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
......
......@@ -837,9 +837,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
final String action = child.getName();
final String sessionId = child.getAttribute("id");
if (sessionId == null) {
break;
}
if (sessionId == null) {
break;
}
if (query == null) {
if (serverMsgId == null) {
serverMsgId = extractStanzaId(account, packet);
......@@ -952,7 +952,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final String id = displayed.getAttribute("id");
final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
if (packet.fromAccount(account) && !selfAddressed) {
dismissNotification(account, counterpart, query);
dismissNotification(account, counterpart, query, id);
if (query == null) {
activateGracePeriod(account);
}
......@@ -993,7 +993,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message = message.prev();
}
if (displayedMessage != null && selfAddressed) {
dismissNotification(account, counterpart, query);
dismissNotification(account, counterpart, query, id);
}
}
}
......@@ -1018,10 +1018,15 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query) {
Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
if (conversation != null && (query == null || query.isCatchup())) {
mXmppConnectionService.markRead(conversation); //TODO only mark messages read that are older than timestamp
final String displayableId = conversation.findMostRecentRemoteDisplayableId();
if (displayableId != null && displayableId.equals(id)) {
mXmppConnectionService.markRead(conversation);
} else {
Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation");
}
}
}
......
......@@ -602,9 +602,6 @@ public class NotificationService {
} catch (SecurityException e) {
Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
}
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
}
mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
setNotificationColor(mBuilder);
mBuilder.setLights(LED_COLOR, 2000, 3000);
......
......@@ -170,6 +170,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
public static final String ACTION_DISMISS_CALL = "dismiss_call";
public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
......@@ -659,6 +660,15 @@ public class XmppConnectionService extends Service {
mJingleConnectionManager.endRtpSession(sessionId);
}
break;
case ACTION_PROVISION_ACCOUNT: {
final String address = intent.getStringExtra("address");
final String password = intent.getStringExtra("password");
if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
break;
}
provisionAccount(address, password);
break;
}
case ACTION_DISMISS_ERROR_NOTIFICATIONS:
dismissErrorNotifications();
break;
......@@ -2180,6 +2190,14 @@ public class XmppConnectionService extends Service {
}
}
private void provisionAccount(final String address, final String password) {
final Jid jid = Jid.ofEscaped(address);
final Account account = new Account(jid, password);
account.setOption(Account.OPTION_DISABLED, true);
Log.d(Config.LOGTAG,jid.asBareJid().toEscapedString()+": provisioning account");
createAccount(account);
}
public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
new Thread(() -> {
try {
......
......@@ -900,7 +900,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private static boolean anyNeedsExternalStoragePermission(final Collection<Attachment> attachments) {
for(final Attachment attachment : attachments) {
for (final Attachment attachment : attachments) {
if (attachment.getType() != Attachment.Type.LOCATION) {
return true;
}
......@@ -1346,7 +1346,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final Contact contact = conversation.getContact();
if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
triggerRtpSession(contact.getAccount(),contact.getJid().asBareJid(),action);
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
} else {
final RtpCapability.Capability capability;
if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) {
......@@ -1436,6 +1436,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
public void attachFile(final int attachmentChoice) {
attachFile(attachmentChoice, true);
}
public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) {
if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) {
return;
......@@ -1449,12 +1453,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return;
}
}
try {
activity.getPreferences().edit()
.putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
.apply();
} catch (IllegalArgumentException e) {
//just do not save
if (updateRecentlyUsed) {
storeRecentlyUsedQuickAction(attachmentChoice);
}
final int encryption = conversation.getNextEncryption();
final int mode = conversation.getMode();
......@@ -1502,6 +1502,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
private void storeRecentlyUsedQuickAction(final int attachmentChoice) {
try {
activity.getPreferences().edit()
.putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString())
.apply();
} catch (IllegalArgumentException e) {
//just do not save
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0) {
......@@ -2134,7 +2144,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return this.binding != null && scrolledToBottom(this.binding.messagesView);
}
private void processExtras(Bundle extras) {
private void processExtras(final Bundle extras) {
final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID);
final String text = extras.getString(Intent.EXTRA_TEXT);
final String nick = extras.getString(ConversationsActivity.EXTRA_NICK);
......@@ -2180,7 +2190,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
if (ConversationsActivity.POST_ACTION_RECORD_VOICE.equals(postInitAction)) {
attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false);
return;
}
final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid);
......@@ -2189,7 +2199,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
private List<Uri> extractUris(Bundle extras) {
private List<Uri> extractUris(final Bundle extras) {
final List<Uri> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
if (uris != null) {
return uris;
......@@ -2202,7 +2212,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
}
private List<Uri> cleanUris(List<Uri> uris) {
private List<Uri> cleanUris(final List<Uri> uris) {
Iterator<Uri> iterator = uris.iterator();
while (iterator.hasNext()) {
final Uri uri = iterator.next();
......@@ -2878,13 +2888,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private void clearPending() {
if (postponedActivityResult.clear()) {
Log.e(Config.LOGTAG, "cleared pending intent with unhandled result left");
if (pendingTakePhotoUri.clear()) {
Log.e(Config.LOGTAG, "cleared pending photo uri");
}
}
if (pendingScrollState.clear()) {
Log.e(Config.LOGTAG, "cleared scroll state");
}
if (pendingTakePhotoUri.clear()) {
Log.e(Config.LOGTAG, "cleared pending photo uri");
}
if (pendingConversationsUuid.clear()) {
Log.e(Config.LOGTAG, "cleared pending conversations uuid");
}
......
......@@ -276,11 +276,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private void releaseProximityWakeLock() {
if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
Log.d(Config.LOGTAG, "releasing proximity wake lock");
this.mProximityWakeLock.release();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
} else {
this.mProximityWakeLock.release();
}
this.mProximityWakeLock = null;
}
}
private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) {
if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
acquireProximityWakeLock();
......@@ -402,10 +406,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
if (jingleRtpConnection != null) {
releaseVideoTracks(jingleRtpConnection);
} else if (!isChangingConfigurations()) {
if (xmppConnectionService != null) {
retractSessionProposal();
}
}
releaseProximityWakeLock();
super.onStop();
......@@ -424,17 +424,20 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
@Override
public void onBackPressed() {
endCall();
super.onBackPressed();
endCall();
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
if (shouldBePictureInPicture()) {
startPictureInPicture();
return;
}
}
retractSessionProposal();
}
@RequiresApi(api = Build.VERSION_CODES.O)
......@@ -445,7 +448,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
.setAspectRatio(new Rational(10, 16))
.build()
);
} catch (IllegalStateException e) {
} catch (final IllegalStateException e) {
//this sometimes happens on Samsung phones (possibly when Knox is enabled)
Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
}
......@@ -467,7 +470,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED
).contains(rtpConnection.getEndUserState());
} catch (IllegalStateException e) {
} catch (final IllegalStateException e) {
return false;
}
}
......
......@@ -11,12 +11,16 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
import com.google.common.base.Strings;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.R;
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;
......@@ -24,29 +28,45 @@ import eu.siacs.conversations.xmpp.Jid;
public class UriHandlerActivity extends AppCompatActivity {
public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning";
private static final int REQUEST_SCAN_QR_CODE = 0x1234;
private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789;
private static final Pattern VCARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n");
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 boolean handled = false;
public static void scan(Activity activity) {
public static void scan(final Activity activity) {
scan(activity, false);
}
public static void scan(final Activity activity, final boolean provisioning) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
Intent intent = new Intent(activity, UriHandlerActivity.class);
final Intent intent = new Intent(activity, UriHandlerActivity.class);
intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
if (provisioning) {
intent.putExtra(EXTRA_ALLOW_PROVISIONING, true);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
} else {
activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
activity.requestPermissions(
new String[]{Manifest.permission.CAMERA},
provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN
);
}
}
public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) {
if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) {
if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
return;
}
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
scan(activity);
if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
scan(activity, true);
} else {
scan(activity);
}
} else {
Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show();
}
......@@ -88,19 +108,19 @@ public class UriHandlerActivity extends AppCompatActivity {
final List<Jid> accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
final String preauth = xmppUri.getParameter("preauth");
final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
final Jid jid = xmppUri.getJid();
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) {
Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show();
return;
}
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth);
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
startActivity(intent);
return;
}
if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth);
if (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;
......@@ -194,22 +214,38 @@ public class UriHandlerActivity extends AppCompatActivity {
finish();
}
private boolean allowProvisioning() {
final Intent launchIntent = getIntent();
return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
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) {
String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
if (result != null) {
if (result.startsWith("BEGIN:VCARD\n")) {
Matcher matcher = VCARD_XMPP_PATTERN.matcher(result);
if (matcher.find()) {
result = matcher.group(2);
}
final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
if (Strings.isNullOrEmpty(result)) {
finish();
return;
}
if (result.startsWith("BEGIN:VCARD\n")) {
final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result);
if (matcher.find()) {
handleUri(Uri.parse(matcher.group(2)), true);
}
Uri uri = Uri.parse(result);
handleUri(uri, true);
finish();
return;
} else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) {
ProvisioningUtils.provision(this, result);
finish();
return;
}
handleUri(Uri.parse(result), true);
}
finish();
}
private static boolean looksLikeJsonObject(final String input) {
return input.charAt(0) == '{' && input.charAt(input.length() - 1) == '}';
}
}
\ No newline at end of file
package eu.siacs.conversations.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
......@@ -139,4 +140,15 @@ public class Compatibility {
Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service");
}
}
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
public static boolean hasFeatureCamera(final Context context) {
final PackageManager packageManager = context.getPackageManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
} else {
return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA);
}
}
}
......@@ -22,6 +22,8 @@ public class XmppUri {
public static final String ACTION_MESSAGE = "message";
public static final String ACTION_REGISTER = "register";
public static final String ACTION_ROSTER = "roster";
public static final String PARAMETER_PRE_AUTH = "preauth";
public static final String PARAMETER_IBR = "ibr";
private static final String OMEMO_URI_PARAM = "omemo-sid-";
protected Uri uri;
protected String jid;
......
......@@ -39,6 +39,7 @@ import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
......@@ -47,7 +48,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.Jid;
public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback {
......@@ -120,7 +120,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>();
private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>();
private final Message message;
private State state = State.NULL;
private StateTransitionException stateTransitionException;
......@@ -237,13 +237,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e);
return;
}
final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap;
final Group originalGroup = rtpContentMap != null ? rtpContentMap.group : null;
final List<String> identificationTags = originalGroup == null ? Collections.emptyList() : 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");
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates = contentMap.contents.entrySet();
if (this.state == State.SESSION_ACCEPTED) {
processCandidates(candidates);
} else {
pendingIceCandidates.push(candidates);
}
receiveCandidates(identificationTags, contentMap.contents.entrySet());
} else {
if (isTerminated()) {
respondOk(jinglePacket);
......@@ -255,7 +254,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
}
private void receiveCandidates(final List<String> identificationTags, final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
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) {
final String ufrag = content.getValue().transport.getAttribute("ufrag");
for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) {
......@@ -267,15 +276,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
continue;
}
final String sdpMid = content.getKey();
final int mLineIndex = identificationTags.indexOf(sdpMid);
final int mLineIndex = indices.indexOf(sdpMid);
final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp);
if (isInState(State.SESSION_ACCEPTED)) {
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
this.webRTCWrapper.addIceCandidate(iceCandidate);
} else {
this.pendingIceCandidates.offer(iceCandidate);
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog");
}
Log.d(Config.LOGTAG, "received candidate: " + iceCandidate);
this.webRTCWrapper.addIceCandidate(iceCandidate);
}
}
}
......@@ -318,8 +322,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket);
final List<String> identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags();
receiveCandidates(identificationTags, contentMap.contents.entrySet());
pendingIceCandidates.push(contentMap.contents.entrySet());
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
sendSessionAccept();
......@@ -364,8 +367,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (transition(State.SESSION_ACCEPTED)) {
respondOk(jinglePacket);
receiveSessionAccept(contentMap);
final List<String> identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags();
receiveCandidates(identificationTags, contentMap.contents.entrySet());
final List<String> identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags();
processCandidates(identificationTags, contentMap.contents.entrySet());
} else {
Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state));
respondOk(jinglePacket);
......@@ -451,9 +454,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void addIceCandidatesFromBlackLog() {
while (!this.pendingIceCandidates.isEmpty()) {
final IceCandidate iceCandidate = this.pendingIceCandidates.poll();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added ICE candidate from back log " + iceCandidate);
this.webRTCWrapper.addIceCandidate(iceCandidate);
processCandidates(this.pendingIceCandidates.poll());
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log");
}
}
......@@ -461,7 +463,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.responderRtpContentMap = rtpContentMap;
this.transitionOrThrow(State.SESSION_ACCEPTED);
final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
Log.d(Config.LOGTAG, sessionAccept.toString());
send(sessionAccept);
}
......@@ -889,6 +890,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
public synchronized void rejectCall() {
if (isTerminated()) {
Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received rejectCall() when session has already been terminated. nothing to do");
return;
}
switch (this.state) {
case PROPOSED:
rejectCallFromProposed();
......
......@@ -6,6 +6,7 @@ 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.Iterables;
import com.google.common.collect.Maps;
......@@ -13,6 +14,7 @@ import com.google.common.collect.Sets;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.List;
import java.util.Map;
import java.util.Set;
......@@ -23,7 +25,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class RtpContentMap {
......@@ -59,6 +60,10 @@ public class RtpContentMap {
}));
}
public List<String> getNames() {
return ImmutableList.copyOf(contents.keySet());
}
void requireContentDescriptions() {
if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available");
......
......@@ -168,8 +168,10 @@ public class SessionDescription {
}
formatBuilder.add(payloadType.getIntId());
mediaAttributes.put("rtpmap", payloadType.toSdpAttribute());
List<RtpDescription.Parameter> parameters = payloadType.getParameters();
if (parameters.size() > 0) {
final List<RtpDescription.Parameter> parameters = payloadType.getParameters();
if (parameters.size() == 1) {
mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0)));
} else if (parameters.size() > 0) {
mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters));
}
for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) {
......
......@@ -214,9 +214,13 @@ public class WebRTCWrapper {
PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions()
);
} catch (final UnsatisfiedLinkError e) {
throw new InitializationException(e);
throw new InitializationException("Unable to initialize PeerConnectionFactory", e);
}
try {
this.eglBase = EglBase.create();
} catch (final RuntimeException e) {
throw new InitializationException("Unable to create EGL base", e);
}
this.eglBase = EglBase.create();
this.context = service;
this.toneManager = service.getJingleConnectionManager().toneManager;
mainHandler.post(() -> {
......@@ -589,8 +593,8 @@ public class WebRTCWrapper {
static class InitializationException extends Exception {
private InitializationException(final Throwable throwable) {
super(throwable);
private InitializationException(final String message, final Throwable throwable) {
super(message, throwable);
}
private InitializationException(final String message) {
......
......@@ -3,6 +3,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas;
import android.util.Pair;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
......@@ -369,7 +370,7 @@ public class RtpDescription extends GenericDescription {
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(id).append(' ');
for (int i = 0; i < parameters.size(); ++i) {
Parameter p = parameters.get(i);
final Parameter p = parameters.get(i);
final String name = p.getParameterName();
Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id));
SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id));
......@@ -386,11 +387,23 @@ public class RtpDescription extends GenericDescription {
return stringBuilder.toString();
}
public static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
public static String toSdpString(final String id, final Parameter parameter) {
final String name = parameter.getParameterName();
final String value = parameter.getParameterValue();
Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
if (Strings.isNullOrEmpty(name)) {
return String.format("%s %s", id, value);
} else {
return String.format("%s %s=%s", id, name, value);
}
}
static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
final String[] pair = sdp.split(" ");
if (pair.length == 2) {
final String id = pair[0];
ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
for (final String parameter : pair[1].split(";")) {
final String[] parts = parameter.split("=", 2);
if (parts.length == 2) {
......
......@@ -70,7 +70,7 @@
<string name="crash_report_message">Надсиланням звіту про відмову ви допомагаєте удосконаленню месенджера.\n<b>Увага:</b> Звіти надсилатимуться розробниками з вашого облікового запису XMPP.</string>
<string name="send_now">Надіслати зараз</string>
<string name="send_never">Ніколи не питати знову</string>
<string name="problem_connecting_to_account">Не можу з\'єднатися з обліковим записом</string>
<string name="problem_connecting_to_account">Неможливо з\'єднатися з обліковим записом</string>
<string name="problem_connecting_to_accounts">Не можу увімкнути режим багатьох облікових записів</string>
<string name="touch_to_fix">Перейти для керування обліковими записами</string>
<string name="attach_file">Долучити файл</string>
......@@ -149,14 +149,14 @@
<string name="account_status_disabled">Тимчасово вимкнено</string>
<string name="account_status_online">У мережі</string>
<string name="account_status_connecting">З\'єднання\u2026</string>
<string name="account_status_offline">Не в мережі</string>
<string name="account_status_offline">Поза мережею</string>
<string name="account_status_unauthorized">Не авторизовано</string>
<string name="account_status_not_found">Сервер не знайдено</string>
<string name="account_status_no_internet">Немає зв\'язку із мережею</string>
<string name="account_status_regis_fail">Не вдалося зареєструватися</string>
<string name="account_status_regis_conflict">Ім\'я користувача вже використовується</string>
<string name="account_status_regis_success">Реєстрацію виконано</string>
<string name="account_status_regis_not_sup">Сервер не підтримує режстрацію</string>
<string name="account_status_regis_not_sup">Сервер не підтримує реєстрацію</string>
<string name="account_status_regis_invalid_token">Неправильний реєстраційний токен</string>
<string name="account_status_tls_error">Узгодження TLS не відбулося</string>
<string name="account_status_policy_violation">Порушення політики</string>
......@@ -182,7 +182,7 @@
<string name="block_jabber_id">Заблокувати адресу XMPP</string>
<string name="account_settings_example_jabber_id">username@example.com</string>
<string name="password">Пароль</string>
<string name="invalid_jid">Це неправильна адреса XMPP</string>
<string name="invalid_jid">Це недійсна адреса XMPP</string>
<string name="error_out_of_memory">Пам\'ять вичерпано. Завелике зображення.</string>
<string name="add_phone_book_text">Бажаєте додати %s до своєї книги контактів?</string>
<string name="server_info_show_more">Інформація про сервер</string>
......@@ -196,10 +196,10 @@
<string name="server_info_pep">XEP-0163: Персональне (піктограми користувачів, OMEMO)</string>
<string name="server_info_http_upload">XEP-0363: Обмін файлами через HTTP</string>
<string name="server_info_push">XEP-0357: Push-повідомлення</string>
<string name="server_info_available">є</string>
<string name="server_info_unavailable">нема</string>
<string name="missing_public_keys">Не вистачає повідомлення публічного ключа.</string>
<string name="last_seen_now">у мережі</string>
<string name="server_info_available">так</string>
<string name="server_info_unavailable">ні</string>
<string name="missing_public_keys">Бракує інформації про публічний ключ</string>
<string name="last_seen_now">зараз у мережі</string>
<string name="last_seen_min">у мережі 1 хвилину тому</string>
<string name="last_seen_mins">у мережі %d хвилин тому</string>
<string name="last_seen_hour">у мережі 1 годину тому</string>
......@@ -368,7 +368,7 @@
<string name="disable_all_accounts">Вимкнути всі облікові записи</string>
<string name="perform_action_with">Здійснити дію з</string>
<string name="no_affiliation">Непов\'язаний</string>
<string name="no_role">Не в мережі</string>
<string name="no_role">Поза мережею</string>
<string name="outcast">Вигнанець</string>
<string name="member">Учасник</string>
<string name="advanced_mode">Розширений режим</string>
......@@ -669,7 +669,7 @@
<string name="received_message_from_stranger">Отримано повідомлення від незнайомця</string>
<string name="block_stranger">Заблокувати невідомий контакт</string>
<string name="block_entire_domain">Заблокувати весь домен</string>
<string name="online_right_now">у мережі просто зараз</string>
<string name="online_right_now">зараз у мережі</string>
<string name="retry_decryption">Спробувати знову розшифрувати</string>
<string name="session_failure">Помилка сесії</string>
<string name="sasl_downgrade">Застарілий механізм SASL</string>
......@@ -838,7 +838,7 @@
<string name="ebook">Електронна книга</string>
<string name="video_original">Оригінал (не стиснений)</string>
<string name="open_with">Відкрити</string>
<string name="set_profile_picture">Фото профілю чату</string>
<string name="set_profile_picture">Світлана профілю</string>
<string name="choose_account">Виберіть обліковий запис</string>
<string name="restore_backup">Відновити з резервної копії</string>
<string name="restore">Відновити</string>
......@@ -902,7 +902,7 @@
<string name="pref_channel_discovery_summary">Більшість користувачів вибирають \'jabber.network\' як одну з кращих пропозицій зі всіх публічних середовищ XMPP.</string>
<string name="pref_channel_discovery">Спосіб пошуку каналів</string>
<string name="backup">Резервне копіювання</string>
<string name="category_about">Про</string>
<string name="category_about">Про застосунок</string>
<string name="please_enable_an_account">Будь ласка, активуйте обліковий запис</string>
<string name="make_call">Здійснити виклик</string>
<string name="rtp_state_incoming_call">Вхідний виклик</string>
......@@ -917,6 +917,7 @@
<string name="rtp_state_ringing">Викликаю</string>
<string name="rtp_state_declined_or_busy">Зайнято</string>
<string name="rtp_state_connectivity_error">Неможливо здійснити виклик</string>
<string name="rtp_state_connectivity_lost_error">З\'єднання втрачено</string>
<string name="rtp_state_retracted">Відхилені виклики</string>
<string name="rtp_state_application_failure">Помилка застосунку</string>
<string name="hang_up">Завершити</string>
......@@ -930,6 +931,7 @@
<string name="missed_call">Пропущені виклики</string>
<string name="audio_call">Голосовий виклик</string>
<string name="video_call">Відеовиклик</string>
<string name="help">Допомога</string>
<string name="microphone_unavailable">Недоступний мікрофон</string>
<string name="only_one_call_at_a_time">Водночас можливо здійснювати лише один виклик.</string>
<string name="return_to_ongoing_call">Назад до активного виклику</string>
......
This diff is collapsed.
package eu.siacs.conversations.utils;
import eu.siacs.conversations.ui.UriHandlerActivity;
public class ProvisioningUtils {
public static void provision(UriHandlerActivity uriHandlerActivity, String result) {
throw new IllegalStateException("Quicksy does not support provisioning");
}
}