diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml
index 1697e401a91b5584bdfb96bcc62406e11a32dbe7..299f65672c4201fda1f83b0e40aadf1b8bd67f4a 100644
--- a/.builds/debian-stable.yml
+++ b/.builds/debian-stable.yml
@@ -22,6 +22,6 @@ tasks:
yes | android/cmdline-tools/tools/bin/sdkmanager --licenses
- build: |
cd cheogram-android
- ./gradlew assembleConversationsFreeCompatDebug
+ ./gradlew assembleCheogramFreeSystemDebug
- assets: |
- mv cheogram-android/build/outputs/apk/conversationsFreeCompat/debug/*.apk cheogram.apk
+ mv cheogram-android/build/outputs/apk/cheogramFreeSystem/debug/*.apk cheogram.apk
diff --git a/build.gradle b/build.gradle
index 9adcfd56692cb4df268bfbebfbb92295bbdb1d3e..78285540ecd45732a3d8a91fef2ac8ca1630c2b6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -141,6 +141,17 @@ android {
dimension "mode"
}
+ cheogram {
+ dimension "mode"
+
+ applicationId = "com.cheogram.android"
+ resValue "string", "applicationId", applicationId
+
+ def appName = "Cheogram"
+ resValue "string", "app_name", appName
+ buildConfigField "String", "APP_NAME", "\"$appName\"";
+ }
+
playstore {
dimension "distribution"
versionNameSuffix "+p"
@@ -200,6 +211,17 @@ android {
srcDir 'src/conversationsFree/java'
}
}
+ cheogramFreeCompat {
+ java {
+ srcDir 'src/freeCompat/java'
+ srcDir 'src/conversationsFree/java'
+ }
+ }
+ cheogramFreeSystem {
+ java {
+ srcDir 'src/conversationsFree/java'
+ }
+ }
conversationsPlaystoreCompat {
java {
srcDir 'src/playstoreCompat/java'
diff --git a/src/cheogram/AndroidManifest.xml b/src/cheogram/AndroidManifest.xml
new file mode 100644
index 0000000000000000000000000000000000000000..62396bed1af27fcdcac16db152115fbd362223ec
--- /dev/null
+++ b/src/cheogram/AndroidManifest.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..65b6804f9b6e7bb4abac560824bf5958a38dbd0b
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java
@@ -0,0 +1,50 @@
+package eu.siacs.conversations.entities;
+
+import com.google.common.base.Preconditions;
+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,
+ }
+
+}
+
diff --git a/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c6ebaafd08e438e48f371d3a0a3c6f61714138e
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java
@@ -0,0 +1,387 @@
+package eu.siacs.conversations.services;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.provider.OpenableColumns;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Stopwatch;
+import com.google.common.io.CountingInputStream;
+
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipException;
+
+import javax.crypto.BadPaddingException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class ImportBackupService extends Service {
+
+ private static final int NOTIFICATION_ID = 21;
+ private static final AtomicBoolean running = new AtomicBoolean(false);
+ private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
+ private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
+ private final Set mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
+ private DatabaseBackend mDatabaseBackend;
+ private NotificationManager notificationManager;
+
+ private static int count(String input, char c) {
+ int count = 0;
+ for (char aChar : input.toCharArray()) {
+ if (aChar == c) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+ final String password = intent.getStringExtra("password");
+ final Uri data = intent.getData();
+ final Uri uri;
+ if (data == null) {
+ final String file = intent.getStringExtra("file");
+ uri = file == null ? null : Uri.fromFile(new File(file));
+ } else {
+ uri = data;
+ }
+
+ if (password == null || password.isEmpty() || uri == null) {
+ return START_NOT_STICKY;
+ }
+ if (running.compareAndSet(false, true)) {
+ executor.execute(() -> {
+ startForegroundService();
+ final boolean success = importBackup(uri, password);
+ stopForeground(true);
+ running.set(false);
+ if (success) {
+ notifySuccess();
+ }
+ stopSelf();
+ });
+ } else {
+ Log.d(Config.LOGTAG, "backup already running");
+ }
+ return START_NOT_STICKY;
+ }
+
+ public boolean getLoadingState() {
+ return running.get();
+ }
+
+ public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) {
+ executor.execute(() -> {
+ final List accounts = mDatabaseBackend.getAccountJids(false);
+ final ArrayList backupFiles = new ArrayList<>();
+ final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
+ for (String app : apps) {
+ final File directory = new File(FileBackend.getBackupDirectory(app));
+ if (!directory.exists() || !directory.isDirectory()) {
+ Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
+ continue;
+ }
+ final File[] files = directory.listFiles();
+ if (files == null) {
+ onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+ return;
+ }
+ for (final File file : files) {
+ if (file.isFile() && file.getName().endsWith(".ceb")) {
+ try {
+ final BackupFile backupFile = BackupFile.read(file);
+ if (accounts.contains(backupFile.getHeader().getJid())) {
+ Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
+ } else {
+ backupFiles.add(backupFile);
+ }
+ } catch (IOException | IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
+ }
+ }
+ }
+ Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString()));
+ onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+ });
+ }
+
+ private void startForegroundService() {
+ startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0));
+ }
+
+ private void updateImportBackupNotification(final long total, final long current) {
+ final int max;
+ final int progress;
+ if (total == 0) {
+ max = 1;
+ progress = 0;
+ } else {
+ max = 100;
+ progress = (int) (current * 100 / total);
+ }
+ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
+ try {
+ notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress));
+ } catch (final RuntimeException e) {
+ Log.d(Config.LOGTAG, "unable to make notification", e);
+ }
+ }
+
+ private Notification createImportBackupNotification(final int max, final int progress) {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.restoring_backup))
+ .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
+ .setProgress(max, progress, max == 1 && progress == 0);
+ return mBuilder.build();
+ }
+
+ private boolean importBackup(final Uri uri, final String password) {
+ Log.d(Config.LOGTAG, "importing backup from " + uri);
+ final Stopwatch stopwatch = Stopwatch.createStarted();
+ try {
+ final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
+ final InputStream inputStream;
+ final String path = uri.getPath();
+ final long fileSize;
+ if ("file".equals(uri.getScheme()) && path != null) {
+ final File file = new File(path);
+ inputStream = new FileInputStream(file);
+ fileSize = file.length();
+ } else {
+ final Cursor returnCursor = getContentResolver().query(uri, null, null, null, null);
+ if (returnCursor == null) {
+ fileSize = 0;
+ } else {
+ returnCursor.moveToFirst();
+ fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE));
+ returnCursor.close();
+ }
+ inputStream = getContentResolver().openInputStream(uri);
+ }
+ if (inputStream == null) {
+ synchronized (mOnBackupProcessedListeners) {
+ for (final OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onBackupRestoreFailed();
+ }
+ }
+ return false;
+ }
+ final CountingInputStream countingInputStream = new CountingInputStream(inputStream);
+ final DataInputStream dataInputStream = new DataInputStream(countingInputStream);
+ final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ Log.d(Config.LOGTAG, backupFileHeader.toString());
+
+ if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) {
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onAccountAlreadySetup();
+ }
+ }
+ return false;
+ }
+
+ final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
+
+ final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
+ final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher);
+
+ final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8));
+ db.beginTransaction();
+ String line;
+ StringBuilder multiLineQuery = null;
+ while ((line = reader.readLine()) != null) {
+ int count = count(line, '\'');
+ if (multiLineQuery != null) {
+ multiLineQuery.append('\n');
+ multiLineQuery.append(line);
+ if (count % 2 == 1) {
+ db.execSQL(multiLineQuery.toString());
+ multiLineQuery = null;
+ updateImportBackupNotification(fileSize, countingInputStream.getCount());
+ }
+ } else {
+ if (count % 2 == 0) {
+ db.execSQL(line);
+ updateImportBackupNotification(fileSize, countingInputStream.getCount());
+ } else {
+ multiLineQuery = new StringBuilder(line);
+ }
+ }
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ final Jid jid = backupFileHeader.getJid();
+ final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()});
+ countCursor.moveToFirst();
+ final int count = countCursor.getInt(0);
+ Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString()));
+ countCursor.close();
+ stopBackgroundService();
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onBackupRestored();
+ }
+ }
+ return true;
+ } catch (final Exception e) {
+ final Throwable throwable = e.getCause();
+ final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ if (reasonWasCrypto) {
+ l.onBackupDecryptionFailed();
+ } else {
+ l.onBackupRestoreFailed();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "error restoring backup " + uri, e);
+ return false;
+ }
+ }
+
+ private void notifySuccess() {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
+ .setContentText(getString(R.string.notification_restored_backup_subtitle))
+ .setAutoCancel(true)
+ .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
+ .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
+ notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
+ }
+
+ private void stopBackgroundService() {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ stopService(intent);
+ }
+
+ public void removeOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.remove(listener);
+ }
+ }
+
+ public void addOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.add(listener);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return this.binder;
+ }
+
+ public interface OnBackupFilesLoaded {
+ void onBackupFilesLoaded(List files);
+ }
+
+ public interface OnBackupProcessed {
+ void onBackupRestored();
+
+ void onBackupDecryptionFailed();
+
+ void onBackupRestoreFailed();
+
+ void onAccountAlreadySetup();
+ }
+
+ public static class BackupFile {
+ private final Uri uri;
+ private final BackupFileHeader header;
+
+ private BackupFile(Uri uri, BackupFileHeader header) {
+ this.uri = uri;
+ this.header = header;
+ }
+
+ private static BackupFile read(File file) throws IOException {
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ fileInputStream.close();
+ return new BackupFile(Uri.fromFile(file), backupFileHeader);
+ }
+
+ public static BackupFile read(final Context context, final Uri uri) throws IOException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(uri);
+ if (inputStream == null) {
+ throw new FileNotFoundException();
+ }
+ final DataInputStream dataInputStream = new DataInputStream(inputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ inputStream.close();
+ return new BackupFile(uri, backupFileHeader);
+ }
+
+ public BackupFileHeader getHeader() {
+ return header;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+ }
+
+ public class ImportBackupServiceBinder extends Binder {
+ public ImportBackupService getService() {
+ return ImportBackupService.this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2a0d17f4acc8caa60e316cc64217e3a3604c9b7
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
@@ -0,0 +1,38 @@
+package eu.siacs.conversations.services;
+
+import android.content.Intent;
+import android.util.Log;
+
+import eu.siacs.conversations.Config;
+
+public class QuickConversationsService extends AbstractQuickConversationsService {
+
+ QuickConversationsService(XmppConnectionService xmppConnectionService) {
+ super(xmppConnectionService);
+ }
+
+ @Override
+ public void considerSync() {
+
+ }
+
+ @Override
+ public void signalAccountStateChange() {
+
+ }
+
+ @Override
+ public boolean isSynchronizing() {
+ return false;
+ }
+
+ @Override
+ public void considerSyncBackground(boolean force) {
+
+ }
+
+ @Override
+ public void handleSmsReceived(Intent intent) {
+ Log.d(Config.LOGTAG,"ignoring received SMS");
+ }
+}
\ No newline at end of file
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..fea92401b76e511d0371e91c173114dc9000426b
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java
@@ -0,0 +1,151 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.databinding.DataBindingUtil;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityEasyInviteBinding;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.BarcodeProvider;
+import eu.siacs.conversations.utils.EasyOnboardingInvite;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested {
+
+ private ActivityEasyInviteBinding binding;
+
+ private EasyOnboardingInvite easyOnboardingInvite;
+
+
+ @Override
+ public void onCreate(final Bundle bundle) {
+ super.onCreate(bundle);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite);
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar(), true);
+ this.binding.shareButton.setOnClickListener(v -> share());
+ if (bundle != null && bundle.containsKey("invite")) {
+ this.easyOnboardingInvite = bundle.getParcelable("invite");
+ if (this.easyOnboardingInvite != null) {
+ showInvite(this.easyOnboardingInvite);
+ return;
+ }
+ }
+ this.showLoading();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu);
+ final MenuItem share = menu.findItem(R.id.action_share);
+ share.setVisible(easyOnboardingInvite != null);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if (menuItem.getItemId() == R.id.action_share) {
+ share();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(menuItem);
+ }
+ }
+
+ private void share() {
+ final String shareText = getString(
+ R.string.easy_invite_share_text,
+ easyOnboardingInvite.getDomain(),
+ easyOnboardingInvite.getShareableLink()
+ );
+ final Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
+ sendIntent.setType("text/plain");
+ startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with)));
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ invalidateOptionsMenu();
+ if (easyOnboardingInvite != null) {
+ showInvite(easyOnboardingInvite);
+ } else {
+ showLoading();
+ }
+ }
+
+ private void showLoading() {
+ this.binding.inProgress.setVisibility(View.VISIBLE);
+ this.binding.invite.setVisibility(View.GONE);
+ }
+
+ private void showInvite(final EasyOnboardingInvite invite) {
+ this.binding.inProgress.setVisibility(View.GONE);
+ this.binding.invite.setVisibility(View.VISIBLE);
+ this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain()));
+ final Point size = new Point();
+ getWindowManager().getDefaultDisplay().getSize(size);
+ final int width = Math.min(size.x, size.y);
+ final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width);
+ binding.qrCode.setImageBitmap(bitmap);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ if (easyOnboardingInvite != null) {
+ bundle.putParcelable("invite", easyOnboardingInvite);
+ }
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (easyOnboardingInvite != null) {
+ return;
+ }
+ final Intent launchIntent = getIntent();
+ final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT);
+ final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra);
+ if (jid == null) {
+ return;
+ }
+ final Account account = xmppConnectionService.findAccountByJid(jid);
+ xmppConnectionService.requestEasyOnboardingInvite(account, this);
+ }
+
+ public static void launch(final Account account, final Activity context) {
+ final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void inviteRequested(EasyOnboardingInvite invite) {
+ this.easyOnboardingInvite = invite;
+ Log.d(Config.LOGTAG, "invite requested");
+ refreshUi();
+ }
+
+ @Override
+ public void inviteRequestFailed(final String message) {
+ runOnUiThread(() -> {
+ if (!Strings.isNullOrEmpty(message)) {
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+ }
+ finish();
+ });
+ }
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1f4d8d11a37ce20132442579bcfcd8fdb50c60a
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java
@@ -0,0 +1,242 @@
+package eu.siacs.conversations.ui;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.databinding.DataBindingUtil;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import java.io.IOException;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
+import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
+import eu.siacs.conversations.services.ImportBackupService;
+import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
+import eu.siacs.conversations.utils.ThemeHelper;
+
+public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
+
+ private ActivityImportBackupBinding binding;
+
+ private BackupFileAdapter backupFileAdapter;
+ private ImportBackupService service;
+
+ private boolean mLoadingState = false;
+
+ private int mTheme;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ this.mTheme = ThemeHelper.find(this);
+ setTheme(this.mTheme);
+ super.onCreate(savedInstanceState);
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
+ setSupportActionBar(binding.toolbar);
+ setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false));
+ this.backupFileAdapter = new BackupFileAdapter();
+ this.binding.list.setAdapter(this.backupFileAdapter);
+ this.backupFileAdapter.setOnItemClickedListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.import_backup, menu);
+ final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file);
+ openBackup.setVisible(!this.mLoadingState);
+ return true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ bundle.putBoolean("loading_state", this.mLoadingState);
+ super.onSaveInstanceState(bundle);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final int theme = ThemeHelper.find(this);
+ if (this.mTheme != theme) {
+ recreate();
+ } else {
+ bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
+ }
+ final Intent intent = getIntent();
+ if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ openBackupFileFromUri(uri, true);
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (this.service != null) {
+ this.service.removeOnBackupProcessedListener(this);
+ }
+ unbindService(this);
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service;
+ this.service = binder.getService();
+ this.service.addOnBackupProcessedListener(this);
+ setLoadingState(this.service.getLoadingState());
+ this.service.loadBackupFiles(this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ this.service = null;
+ }
+
+ @Override
+ public void onBackupFilesLoaded(final List files) {
+ runOnUiThread(() -> backupFileAdapter.setFiles(files));
+ }
+
+ @Override
+ public void onClick(final ImportBackupService.BackupFile backupFile) {
+ showEnterPasswordDialog(backupFile, false);
+ }
+
+ private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
+ try {
+ final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
+ showEnterPasswordDialog(backupFile, finishOnCancel);
+ } catch (final IOException | IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
+ Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
+ }
+ }
+
+ private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
+ final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
+ Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri());
+ enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setView(enterPasswordBinding.getRoot());
+ builder.setTitle(R.string.enter_password);
+ builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
+ if (finishOnCancel) {
+ finish();
+ }
+ });
+ builder.setPositiveButton(R.string.restore, null);
+ builder.setCancelable(false);
+ final AlertDialog dialog = builder.create();
+ dialog.setOnShowListener((d) -> {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
+ final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
+ if (password.isEmpty()) {
+ enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password));
+ return;
+ }
+ final Uri uri = backupFile.getUri();
+ Intent intent = new Intent(this, ImportBackupService.class);
+ intent.setAction(Intent.ACTION_SEND);
+ intent.putExtra("password", password);
+ if ("file".equals(uri.getScheme())) {
+ intent.putExtra("file", uri.getPath());
+ } else {
+ intent.setData(uri);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ setLoadingState(true);
+ ContextCompat.startForegroundService(this, intent);
+ d.dismiss();
+ });
+ });
+ dialog.show();
+ }
+
+ private void setLoadingState(final boolean loadingState) {
+ binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
+ binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
+ setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
+ configureActionBar(getSupportActionBar(), !loadingState);
+ this.mLoadingState = loadingState;
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == 0xbac) {
+ openBackupFileFromUri(intent.getData(), false);
+ }
+ }
+ }
+
+ @Override
+ public void onAccountAlreadySetup() {
+ runOnUiThread(() -> {
+ setLoadingState(false);
+ Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show();
+ });
+ }
+
+ @Override
+ public void onBackupRestored() {
+ runOnUiThread(() -> {
+ Intent intent = new Intent(this, ConversationActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ @Override
+ public void onBackupDecryptionFailed() {
+ runOnUiThread(() -> {
+ setLoadingState(false);
+ Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
+ });
+ }
+
+ @Override
+ public void onBackupRestoreFailed() {
+ runOnUiThread(() -> {
+ setLoadingState(false);
+ Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_open_backup_file) {
+ openBackupFile();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void openBackupFile() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
+ }
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..3419d8fc95638f975ecf630e46cc442ca92ee652
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java
@@ -0,0 +1,164 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.databinding.DataBindingUtil;
+
+import java.security.SecureRandom;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.MagicCreateBinding;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.InstallReferrerUtils;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class MagicCreateActivity extends XmppActivity implements TextWatcher {
+
+ public static final String EXTRA_DOMAIN = "domain";
+ public static final String EXTRA_PRE_AUTH = "pre_auth";
+ public static final String EXTRA_USERNAME = "username";
+
+ private MagicCreateBinding binding;
+ private String domain;
+ private String username;
+ private String preAuth;
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final int theme = findTheme();
+ if (this.mTheme != theme) {
+ recreate();
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ final Intent data = getIntent();
+ this.domain = data == null ? null : data.getStringExtra(EXTRA_DOMAIN);
+ this.preAuth = data == null ? null : data.getStringExtra(EXTRA_PRE_AUTH);
+ this.username = data == null ? null : data.getStringExtra(EXTRA_USERNAME);
+ if (getResources().getBoolean(R.bool.portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+ super.onCreate(savedInstanceState);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create);
+ setSupportActionBar(this.binding.toolbar);
+ configureActionBar(getSupportActionBar(), this.domain == null);
+ if (username != null && domain != null) {
+ binding.title.setText(R.string.your_server_invitation);
+ binding.instructions.setText(getString(R.string.magic_create_text_fixed, domain));
+ binding.finePrint.setVisibility(View.INVISIBLE);
+ binding.username.setEnabled(false);
+ binding.username.setText(this.username);
+ updateFullJidInformation(this.username);
+ } else if (domain != null) {
+ binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain));
+ binding.finePrint.setVisibility(View.INVISIBLE);
+ }
+ binding.createAccount.setOnClickListener(v -> {
+ try {
+ final String username = binding.username.getText().toString();
+ final Jid jid;
+ final boolean fixedUsername;
+ if (this.domain != null && this.username != null) {
+ fixedUsername = true;
+ jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain);
+ } else if (this.domain != null) {
+ fixedUsername = false;
+ jid = Jid.ofLocalAndDomainEscaped(username, this.domain);
+ } else {
+ fixedUsername = false;
+ jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN);
+ }
+ if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) {
+ binding.username.setError(getString(R.string.invalid_username));
+ binding.username.requestFocus();
+ } else {
+ binding.username.setError(null);
+ Account account = xmppConnectionService.findAccountByJid(jid);
+ if (account == null) {
+ account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
+ account.setOption(Account.OPTION_REGISTER, true);
+ account.setOption(Account.OPTION_DISABLED, true);
+ account.setOption(Account.OPTION_MAGIC_CREATE, true);
+ account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername);
+ if (this.preAuth != null) {
+ account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth);
+ }
+ xmppConnectionService.createAccount(account);
+ }
+ Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid().asBareJid().toString());
+ intent.putExtra("init", true);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show();
+ StartConversationActivity.addInviteUri(intent, getIntent());
+ startActivity(intent);
+ }
+ } catch (IllegalArgumentException e) {
+ binding.username.setError(getString(R.string.invalid_username));
+ binding.username.requestFocus();
+ }
+ });
+ binding.username.addTextChangedListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ InstallReferrerUtils.markInstallReferrerExecuted(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ updateFullJidInformation(s.toString());
+ }
+
+ private void updateFullJidInformation(final String username) {
+ if (username.trim().isEmpty()) {
+ binding.fullJid.setVisibility(View.INVISIBLE);
+ } else {
+ try {
+ binding.fullJid.setVisibility(View.VISIBLE);
+ final Jid jid;
+ if (this.domain == null) {
+ jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN);
+ } else {
+ jid = Jid.ofLocalAndDomainEscaped(username, this.domain);
+ }
+ binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString()));
+ } catch (IllegalArgumentException e) {
+ binding.fullJid.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1ee451be7ac01ac8a25b3e7fd7cddedf6885750
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,428 @@
+package eu.siacs.conversations.ui;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Pair;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
+public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
+
+ private final String STATE_SELECTED_ACCOUNT = "selected_account";
+
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
+
+ protected Account selectedAccount = null;
+ protected Jid selectedAccountJid = null;
+
+ protected final List accountList = new ArrayList<>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+ protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
+
+ protected Pair mPostponedActivityResult = null;
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ synchronized (this.accountList) {
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ }
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
+ actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
+ }
+ invalidateOptionsMenu();
+ mAccountAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_manage_accounts);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ configureActionBar(getSupportActionBar());
+ if (savedInstanceState != null) {
+ String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
+ if (jid != null) {
+ try {
+ this.selectedAccountJid = Jid.ofEscaped(jid);
+ } catch (IllegalArgumentException e) {
+ this.selectedAccountJid = null;
+ }
+ }
+ }
+
+ accountListView = findViewById(R.id.account_list);
+ this.mAccountAdapter = new AccountAdapter(this, accountList);
+ accountListView.setAdapter(this.mAccountAdapter);
+ accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
+ registerForContextMenu(accountListView);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final int theme = findTheme();
+ if (this.mTheme != theme) {
+ recreate();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(final Bundle savedInstanceState) {
+ if (selectedAccount != null) {
+ savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString());
+ }
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ManageAccountActivity.this.getMenuInflater().inflate(
+ R.menu.manageaccounts_context, menu);
+ AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+ this.selectedAccount = accountList.get(acmi.position);
+ if (this.selectedAccount.isEnabled()) {
+ menu.findItem(R.id.mgmt_account_enable).setVisible(false);
+ menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
+ } else {
+ menu.findItem(R.id.mgmt_account_disable).setVisible(false);
+ menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
+ menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
+ }
+ menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toEscapedString());
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (selectedAccountJid != null) {
+ this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
+ }
+ refreshUiReal();
+ if (this.mPostponedActivityResult != null) {
+ this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+ }
+ if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
+ if (mInvokedAddAccount.compareAndSet(false, true)) {
+ addAccountFromKey();
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.manageaccounts, menu);
+ MenuItem enableAll = menu.findItem(R.id.action_enable_all);
+ MenuItem addAccount = menu.findItem(R.id.action_add_account);
+ MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
+
+ if (Config.X509_VERIFICATION) {
+ addAccount.setVisible(false);
+ addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ }
+
+ if (!accountsLeftToEnable()) {
+ enableAll.setVisible(false);
+ }
+ MenuItem disableAll = menu.findItem(R.id.action_disable_all);
+ if (!accountsLeftToDisable()) {
+ disableAll.setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.mgmt_account_publish_avatar:
+ publishAvatar(selectedAccount);
+ return true;
+ case R.id.mgmt_account_disable:
+ disableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_enable:
+ enableAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_delete:
+ deleteAccount(selectedAccount);
+ return true;
+ case R.id.mgmt_account_announce_pgp:
+ publishOpenPGPPublicKey(selectedAccount);
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (MenuDoubleTabUtil.shouldIgnoreTap()) {
+ return false;
+ }
+ switch (item.getItemId()) {
+ case R.id.action_add_account:
+ startActivity(new Intent(this, EditAccountActivity.class));
+ break;
+ case R.id.action_import_backup:
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ }
+ break;
+ case R.id.action_disable_all:
+ disableAllAccounts();
+ break;
+ case R.id.action_enable_all:
+ enableAllAccounts();
+ break;
+ case R.id.action_add_account_with_cert:
+ addAccountFromKey();
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ switch (requestCode) {
+ case REQUEST_IMPORT_BACKUP:
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ break;
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+ if (writeGranted(grantResults, permissions)) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.restartFileObserver();
+ }
+ }
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ if (xmppConnectionService.getConversations().size() == 0) {
+ Intent contactsIntent = new Intent(this,
+ StartConversationActivity.class);
+ contactsIntent.setFlags(
+ // if activity exists in stack, pop the stack and go back to it
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ // otherwise, make a new task for it
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ // don't use the new activity animation; finish
+ // animation runs instead
+ Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivity(contactsIntent);
+ finish();
+ return true;
+ } else {
+ return super.onNavigateUp();
+ }
+ }
+
+ @Override
+ public void onClickTglAccountState(Account account, boolean enable) {
+ if (enable) {
+ enableAccount(account);
+ } else {
+ disableAccount(account);
+ }
+ }
+
+ private void addAccountFromKey() {
+ try {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void publishAvatar(Account account) {
+ Intent intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+ startActivity(intent);
+ }
+
+ private void disableAllAccounts() {
+ List list = new ArrayList<>();
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (account.isEnabled()) {
+ list.add(account);
+ }
+ }
+ }
+ for (Account account : list) {
+ disableAccount(account);
+ }
+ }
+
+ private boolean accountsLeftToDisable() {
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (account.isEnabled()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private boolean accountsLeftToEnable() {
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (!account.isEnabled()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private void enableAllAccounts() {
+ List list = new ArrayList<>();
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (!account.isEnabled()) {
+ list.add(account);
+ }
+ }
+ }
+ for (Account account : list) {
+ enableAccount(account);
+ }
+ }
+
+ private void disableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, true);
+ if (!xmppConnectionService.updateAccount(account)) {
+ Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void enableAccount(Account account) {
+ account.setOption(Account.OPTION_DISABLED, false);
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.resetEverything();
+ }
+ if (!xmppConnectionService.updateAccount(account)) {
+ Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void publishOpenPGPPublicKey(Account account) {
+ if (ManageAccountActivity.this.hasPgp()) {
+ announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
+ } else {
+ this.showInstallPgpDialog();
+ }
+ }
+
+ private void deleteAccount(final Account account) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
+ builder.setPositiveButton(getString(R.string.delete),
+ (dialog, which) -> {
+ xmppConnectionService.deleteAccount(account);
+ selectedAccount = null;
+ if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+ WelcomeActivity.launch(this);
+ }
+ });
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.create().show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (xmppConnectionServiceBound) {
+ if (requestCode == REQUEST_CHOOSE_PGP_ID) {
+ if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
+ selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
+ announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
+ } else {
+ choosePgpSignId(selectedAccount);
+ }
+ } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
+ }
+ this.mPostponedActivityResult = null;
+ } else {
+ this.mPostponedActivityResult = new Pair<>(requestCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void alias(final String alias) {
+ if (alias != null) {
+ xmppConnectionService.createAccountFromKey(alias, this);
+ }
+ }
+
+ @Override
+ public void onAccountCreated(final Account account) {
+ final Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid().asBareJid().toString());
+ intent.putExtra("init", true);
+ startActivity(intent);
+ }
+
+ @Override
+ public void informUser(final int r) {
+ runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
+ }
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..06320d33deea510e9edd2b139757ce2ec8c08373
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java
@@ -0,0 +1,104 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityPickServerBinding;
+import eu.siacs.conversations.entities.Account;
+
+public class PickServerActivity extends XmppActivity {
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final int theme = findTheme();
+ if (this.mTheme != theme) {
+ recreate();
+ }
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ startActivity(new Intent(this, WelcomeActivity.class));
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ startActivity(new Intent(this, WelcomeActivity.class));
+ super.onBackPressed();
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ if (intent != null) {
+ setIntent(intent);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ if (getResources().getBoolean(R.bool.portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+ super.onCreate(savedInstanceState);
+ ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server);
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ binding.useCim.setOnClickListener(v -> {
+ final Intent intent = new Intent(this, MagicCreateActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ addInviteUri(intent);
+ startActivity(intent);
+ });
+ binding.useOwnProvider.setOnClickListener(v -> {
+ List accounts = xmppConnectionService.getAccounts();
+ Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, true);
+ if (accounts.size() == 1) {
+ intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
+ intent.putExtra("init", true);
+ } else if (accounts.size() >= 1) {
+ intent = new Intent(this, ManageAccountActivity.class);
+ }
+ addInviteUri(intent);
+ startActivity(intent);
+ });
+
+ }
+
+ public void addInviteUri(Intent intent) {
+ StartConversationActivity.addInviteUri(intent, getIntent());
+ }
+
+ public static void launch(AppCompatActivity activity) {
+ Intent intent = new Intent(activity, PickServerActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ activity.startActivity(intent);
+ activity.overridePendingTransition(0, 0);
+ }
+
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..762dfbb422780c20293eec13c3acdcd846d2595e
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java
@@ -0,0 +1,90 @@
+package eu.siacs.conversations.ui;
+
+import android.os.Bundle;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class ShareViaAccountActivity extends XmppActivity {
+ public static final String EXTRA_CONTACT = "contact";
+ public static final String EXTRA_BODY = "body";
+
+ protected final List accountList = new ArrayList<>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+
+ @Override
+ protected void refreshUiReal() {
+ synchronized (this.accountList) {
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ }
+ mAccountAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_manage_accounts);
+ setSupportActionBar(findViewById(R.id.toolbar));
+ configureActionBar(getSupportActionBar());
+ accountListView = findViewById(R.id.account_list);
+ this.mAccountAdapter = new AccountAdapter(this, accountList, false);
+ accountListView.setAdapter(this.mAccountAdapter);
+ accountListView.setOnItemClickListener((arg0, view, position, arg3) -> {
+ final Account account = accountList.get(position);
+ final String body = getIntent().getStringExtra(EXTRA_BODY);
+
+ try {
+ final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT));
+ final Conversation conversation = xmppConnectionService.findOrCreateConversation(
+ account, contact, false, false);
+ switchToConversation(conversation, body);
+ } catch (IllegalArgumentException e) {
+ // ignore error
+ }
+
+ finish();
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ final int theme = findTheme();
+ if (this.mTheme != theme) {
+ recreate();
+ }
+ }
+
+ @Override
+ void onBackendConnected() {
+ final int numAccounts = xmppConnectionService.getAccounts().size();
+
+ if (numAccounts == 1) {
+ final String body = getIntent().getStringExtra(EXTRA_BODY);
+ final Account account = xmppConnectionService.getAccounts().get(0);
+
+ try {
+ final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT));
+ final Conversation conversation = xmppConnectionService.findOrCreateConversation(
+ account, contact, false, false);
+ switchToConversation(conversation, body);
+ } catch (IllegalArgumentException e) {
+ // ignore error
+ }
+
+ finish();
+ } else {
+ refreshUiReal();
+ }
+ }
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f652ce8e522bef41e4631c504aaacf3c22647cf
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java
@@ -0,0 +1,234 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
+
+import java.util.Arrays;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+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;
+import eu.siacs.conversations.xmpp.Jid;
+
+import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+
+public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback {
+
+ private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
+
+ private XmppUri inviteUri;
+
+ public static void launch(AppCompatActivity activity) {
+ Intent intent = new Intent(activity, WelcomeActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ activity.startActivity(intent);
+ activity.overridePendingTransition(0, 0);
+ }
+
+ public void onInstallReferrerDiscovered(final Uri referrer) {
+ Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer);
+ 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 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;
+ }
+ this.inviteUri = xmppUri;
+ }
+
+ @Override
+ protected void refreshUiReal() {
+
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final int theme = findTheme();
+ if (this.mTheme != theme) {
+ recreate();
+ }
+ new InstallReferrerUtils(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ if (intent != null) {
+ setIntent(intent);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ if (getResources().getBoolean(R.bool.portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+ super.onCreate(savedInstanceState);
+ ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome);
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar(), false);
+ binding.registerNewAccount.setOnClickListener(v -> {
+ final Intent intent = new Intent(this, PickServerActivity.class);
+ addInviteUri(intent);
+ startActivity(intent);
+ });
+ binding.useExisting.setOnClickListener(v -> {
+ final List accounts = xmppConnectionService.getAccounts();
+ Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
+ intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
+ if (accounts.size() == 1) {
+ intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
+ intent.putExtra("init", true);
+ } else if (accounts.size() >= 1) {
+ intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
+ }
+ addInviteUri(intent);
+ startActivity(intent);
+ });
+
+ }
+
+ @Override
+ 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(Compatibility.hasFeatureCamera(this));
+ return super.onCreateOptionsMenu(menu);
+ }
+
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_import_backup:
+ if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ }
+ break;
+ case R.id.action_scan_qr_code:
+ UriHandlerActivity.scan(this, true);
+ break;
+ case R.id.action_add_account_with_cert:
+ addAccountFromKey();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void addAccountFromKey() {
+ try {
+ KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void alias(final String alias) {
+ if (alias != null) {
+ xmppConnectionService.createAccountFromKey(alias, this);
+ }
+ }
+
+ @Override
+ public void onAccountCreated(final Account account) {
+ final Intent intent = new Intent(this, EditAccountActivity.class);
+ intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
+ intent.putExtra("init", true);
+ addInviteUri(intent);
+ startActivity(intent);
+ }
+
+ @Override
+ public void informUser(final int r) {
+ runOnUiThread(() -> Toast.makeText(this, r, Toast.LENGTH_LONG).show());
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
+ if (grantResults.length > 0) {
+ if (allGranted(grantResults)) {
+ switch (requestCode) {
+ case REQUEST_IMPORT_BACKUP:
+ startActivity(new Intent(this, ImportBackupActivity.class));
+ break;
+ }
+ } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+ if (writeGranted(grantResults, permissions)) {
+ if (xmppConnectionService != null) {
+ xmppConnectionService.restartFileObserver();
+ }
+ }
+ }
+
+ public void addInviteUri(Intent to) {
+ final Intent from = getIntent();
+ if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) {
+ final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI);
+ to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite);
+ } else if (this.inviteUri != null) {
+ Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow");
+ to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString());
+ }
+ }
+
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..9857dcd8ac34e664c38f8f68a99366d9f139bfe5
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java
@@ -0,0 +1,170 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.text.format.DateUtils;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.AccountRowBinding;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.ImportBackupService;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class BackupFileAdapter extends RecyclerView.Adapter {
+
+ private OnItemClickedListener listener;
+
+ private final List files = new ArrayList<>();
+
+
+ @NonNull
+ @Override
+ public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) {
+ final ImportBackupService.BackupFile backupFile = files.get(position);
+ final BackupFileHeader header = backupFile.getHeader();
+ backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString());
+ backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s",header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)));
+ backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE);
+ backupFileViewHolder.binding.getRoot().setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onClick(backupFile);
+ }
+ });
+ loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage);
+ }
+
+ @Override
+ public int getItemCount() {
+ return files.size();
+ }
+
+ public void setFiles(List files) {
+ this.files.clear();
+ this.files.addAll(files);
+ notifyDataSetChanged();
+ }
+
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ this.listener = listener;
+ }
+
+ static class BackupFileViewHolder extends RecyclerView.ViewHolder {
+ private final AccountRowBinding binding;
+
+ BackupFileViewHolder(AccountRowBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ }
+
+ public interface OnItemClickedListener {
+ void onClick(ImportBackupService.BackupFile backupFile);
+ }
+
+ static class BitmapWorkerTask extends AsyncTask {
+ private final WeakReference imageViewReference;
+ private Jid jid = null;
+ private final int size;
+
+ BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics();
+ this.size = ((int) (48 * metrics.density));
+ }
+
+ @Override
+ protected Bitmap doInBackground(Jid... params) {
+ this.jid = params[0];
+ return AvatarService.get(this.jid, size);
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null && !isCancelled()) {
+ final ImageView imageView = imageViewReference.get();
+ if (imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setBackgroundColor(0x00000000);
+ }
+ }
+ }
+ }
+
+ private void loadAvatar(Jid jid, ImageView imageView) {
+ if (cancelPotentialWork(jid, imageView)) {
+ imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString()));
+ imageView.setImageDrawable(null);
+ final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task);
+ imageView.setImageDrawable(asyncDrawable);
+ try {
+ task.execute(jid);
+ } catch (final RejectedExecutionException ignored) {
+ }
+ }
+ }
+
+ private static boolean cancelPotentialWork(Jid jid, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Jid oldJid = bitmapWorkerTask.jid;
+ if (oldJid == null || jid != oldJid) {
+ bitmapWorkerTask.cancel(true);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncDrawable) {
+ final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference bitmapWorkerTaskReference;
+
+ AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..593291d95374f4be2f1722b8c46a03fe34ed28dc
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java
@@ -0,0 +1,43 @@
+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 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);
+ }
+
+}
diff --git a/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..fb088234a24e10ea315bafebd910e28df9caa270
--- /dev/null
+++ b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java
@@ -0,0 +1,77 @@
+package eu.siacs.conversations.utils;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.ConversationsActivity;
+import eu.siacs.conversations.ui.EditAccountActivity;
+import eu.siacs.conversations.ui.MagicCreateActivity;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.ui.PickServerActivity;
+import eu.siacs.conversations.ui.StartConversationActivity;
+import eu.siacs.conversations.ui.WelcomeActivity;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class SignupUtils {
+
+ public static boolean isSupportTokenRegistry() {
+ return true;
+ }
+
+ public static Intent getTokenRegistrationIntent(final Activity activity, Jid jid, String preAuth) {
+ final Intent intent = new Intent(activity, MagicCreateActivity.class);
+ if (jid.isDomainJid()) {
+ intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString());
+ } else {
+ intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString());
+ intent.putExtra(MagicCreateActivity.EXTRA_USERNAME, jid.getEscapedLocal());
+ }
+ intent.putExtra(MagicCreateActivity.EXTRA_PRE_AUTH, preAuth);
+ return intent;
+ }
+
+ public static Intent getSignUpIntent(final Activity activity) {
+ return getSignUpIntent(activity, false);
+ }
+
+ public static Intent getSignUpIntent(final Activity activity, final boolean toServerChooser) {
+ final Intent intent;
+ if (toServerChooser) {
+ intent = new Intent(activity, PickServerActivity.class);
+ } else {
+ intent = new Intent(activity, WelcomeActivity.class);
+ }
+ return intent;
+ }
+
+ public static Intent getRedirectionIntent(final ConversationsActivity activity) {
+ final XmppConnectionService service = activity.xmppConnectionService;
+ Account pendingAccount = AccountUtils.getPendingAccount(service);
+ Intent intent;
+ if (pendingAccount != null) {
+ intent = new Intent(activity, EditAccountActivity.class);
+ intent.putExtra("jid", pendingAccount.getJid().asBareJid().toString());
+ if (!pendingAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
+ intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, pendingAccount.isOptionSet(Account.OPTION_REGISTER));
+ }
+ } else {
+ if (service.getAccounts().size() == 0) {
+ if (Config.X509_VERIFICATION) {
+ intent = new Intent(activity, ManageAccountActivity.class);
+ } else if (Config.MAGIC_CREATE_DOMAIN != null) {
+ intent = getSignUpIntent(activity);
+ } else {
+ intent = new Intent(activity, EditAccountActivity.class);
+ }
+ } else {
+ intent = new Intent(activity, StartConversationActivity.class);
+ }
+ }
+ intent.putExtra("init", true);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ return intent;
+ }
+}
\ No newline at end of file
diff --git a/src/cheogram/new_launcher-web.png b/src/cheogram/new_launcher-web.png
new file mode 100644
index 0000000000000000000000000000000000000000..76057f9fea7b2eded734a58069a67c575d466167
Binary files /dev/null and b/src/cheogram/new_launcher-web.png differ
diff --git a/src/cheogram/res/drawable-hdpi/ic_notification.png b/src/cheogram/res/drawable-hdpi/ic_notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..32325e5f79569e3034e4f7ddee5fd07978b85a86
Binary files /dev/null and b/src/cheogram/res/drawable-hdpi/ic_notification.png differ
diff --git a/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..18730f12f8b7a94fc49b26a916aa877091d3a9c2
Binary files /dev/null and b/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/cheogram/res/drawable-mdpi/ic_notification.png b/src/cheogram/res/drawable-mdpi/ic_notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..379720e94faef2a64ae5ec6668eb99fe908d3f40
Binary files /dev/null and b/src/cheogram/res/drawable-mdpi/ic_notification.png differ
diff --git a/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ec62cd34b2c63234ff5593637d5105bdb253343
Binary files /dev/null and b/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/cheogram/res/drawable-xhdpi/ic_notification.png b/src/cheogram/res/drawable-xhdpi/ic_notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..e14da5dc66a114f24da41884ee6675c3cb2a5f8c
Binary files /dev/null and b/src/cheogram/res/drawable-xhdpi/ic_notification.png differ
diff --git a/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0a1509a16df382eb674cddbaf52e6e70292b30c
Binary files /dev/null and b/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/cheogram/res/drawable-xxhdpi/ic_notification.png b/src/cheogram/res/drawable-xxhdpi/ic_notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..6adbc8f648ad35bc7176b6ac5e6c989634068647
Binary files /dev/null and b/src/cheogram/res/drawable-xxhdpi/ic_notification.png differ
diff --git a/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..20d015751f5e7a7654fecd4d1beb6ccdb1033f8b
Binary files /dev/null and b/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_notification.png b/src/cheogram/res/drawable-xxxhdpi/ic_notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..65d106b8c3bb408e7385900b93ff5481e9ce6e93
Binary files /dev/null and b/src/cheogram/res/drawable-xxxhdpi/ic_notification.png differ
diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..a789520baa7f4b65fd73ae78a57ec50b9d645e3c
Binary files /dev/null and b/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/cheogram/res/drawable/background.xml b/src/cheogram/res/drawable/background.xml
new file mode 100644
index 0000000000000000000000000000000000000000..32bc72f1c2329d73914f9930fddc38c244397ef2
--- /dev/null
+++ b/src/cheogram/res/drawable/background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/src/cheogram/res/drawable/bar_logo.xml b/src/cheogram/res/drawable/bar_logo.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2ce2ae9f531625c3065d1cacbd2eb378c9a612d7
--- /dev/null
+++ b/src/cheogram/res/drawable/bar_logo.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/cheogram/res/drawable/ic_launcher.xml b/src/cheogram/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4be18371e4589579fe65ad4763e9f399862607a2
--- /dev/null
+++ b/src/cheogram/res/drawable/ic_launcher.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cheogram/res/drawable/main_logo.xml b/src/cheogram/res/drawable/main_logo.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6fb672cd3c4426fa4e6ef1209eefbd584bbd3e81
--- /dev/null
+++ b/src/cheogram/res/drawable/main_logo.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cheogram/res/drawable/splash_logo.xml b/src/cheogram/res/drawable/splash_logo.xml
new file mode 100644
index 0000000000000000000000000000000000000000..500d002e446e54153c4bc122f36152bdcd2ca79a
--- /dev/null
+++ b/src/cheogram/res/drawable/splash_logo.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/src/cheogram/res/layout/activity_easy_invite.xml b/src/cheogram/res/layout/activity_easy_invite.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8bbf11c037e00c299f152d5084093335d7f5f607
--- /dev/null
+++ b/src/cheogram/res/layout/activity_easy_invite.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/layout/activity_import_backup.xml b/src/cheogram/res/layout/activity_import_backup.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5435c0f729fdb46cddebef8097a48c0f8d4651ad
--- /dev/null
+++ b/src/cheogram/res/layout/activity_import_backup.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/layout/activity_pick_server.xml b/src/cheogram/res/layout/activity_pick_server.xml
new file mode 100644
index 0000000000000000000000000000000000000000..16be52ec458ea923379a88375340047ecc91535e
--- /dev/null
+++ b/src/cheogram/res/layout/activity_pick_server.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/layout/activity_welcome.xml b/src/cheogram/res/layout/activity_welcome.xml
new file mode 100644
index 0000000000000000000000000000000000000000..33076858adae30fef5889598b12ccd4f03ef1547
--- /dev/null
+++ b/src/cheogram/res/layout/activity_welcome.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/layout/dialog_enter_password.xml b/src/cheogram/res/layout/dialog_enter_password.xml
new file mode 100644
index 0000000000000000000000000000000000000000..40f3ba34dec69b1df1cbe7448bd4a8ac9a00f531
--- /dev/null
+++ b/src/cheogram/res/layout/dialog_enter_password.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/layout/magic_create.xml b/src/cheogram/res/layout/magic_create.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cc03370628b526375480bc8df1148792a735207f
--- /dev/null
+++ b/src/cheogram/res/layout/magic_create.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cheogram/res/menu/easy_onboarding_invite.xml b/src/cheogram/res/menu/easy_onboarding_invite.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0e086b5150abfde6c0ee40430afc05582321e660
--- /dev/null
+++ b/src/cheogram/res/menu/easy_onboarding_invite.xml
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/src/cheogram/res/menu/manageaccounts.xml b/src/cheogram/res/menu/manageaccounts.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5a26beaf922ca46dd715c500fb7f8a8dfdcc4ead
--- /dev/null
+++ b/src/cheogram/res/menu/manageaccounts.xml
@@ -0,0 +1,32 @@
+
+
\ No newline at end of file
diff --git a/src/cheogram/res/menu/welcome_menu.xml b/src/cheogram/res/menu/welcome_menu.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f07a2b91ebc66f126fc16f6ccd9d97c6d91d8de3
--- /dev/null
+++ b/src/cheogram/res/menu/welcome_menu.xml
@@ -0,0 +1,22 @@
+
\ No newline at end of file
diff --git a/src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml b/src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml
new file mode 100644
index 0000000000000000000000000000000000000000..33ec1eaa73e057a0cb38cd55d72d36118820fbaa
--- /dev/null
+++ b/src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml b/src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml
new file mode 100644
index 0000000000000000000000000000000000000000..33ec1eaa73e057a0cb38cd55d72d36118820fbaa
--- /dev/null
+++ b/src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/cheogram/res/values-ar/strings.xml b/src/cheogram/res/values-ar/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1e820a9ccff02fac4d7188668374088eede8f605
--- /dev/null
+++ b/src/cheogram/res/values-ar/strings.xml
@@ -0,0 +1,6 @@
+
+
+ اختر مزود خدمة XMPP الخاص بك
+ استخدِم conversations.im
+ أنشئ حسابًا جديدًا
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-bg/strings.xml b/src/cheogram/res/values-bg/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2981a6542b783e934c509c4b4c433ec53929541e
--- /dev/null
+++ b/src/cheogram/res/values-bg/strings.xml
@@ -0,0 +1,17 @@
+
+
+ Изберете вашият XMPP доставчик
+ Използвайте conversations.im
+ Създаване не нов профил
+ Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили.
+
+ XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи добре с Conversations.
+ Бяхте поканен(а) в %1$s. Ще Ви преведем през процеса на създаване на акаунт.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP.
+ Бяхте поканен(а) в %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на акаунт.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP.
+ Вашата покана за сървъра
+ Неправилно форматиран код за достъп
+ Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s.
+ Ако контактът Ви е наблизо, може да сканира кода по-долу, за да приеме поканата Ви.
+ Присъедини се в %1$s и си пиши с мен: %2$s
+ Споделяне на поканата чрез…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-bn-rIN/strings.xml b/src/cheogram/res/values-bn-rIN/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c99df7cfeeff919404d3c12cf97ea0c80afe130f
--- /dev/null
+++ b/src/cheogram/res/values-bn-rIN/strings.xml
@@ -0,0 +1,16 @@
+
+
+ XMPP সার্ভার নির্বাচন করুন
+ conversations.im-ই ব্যবহার করা যাক
+ নতুন অ্যকাউন্ট তৈরী করা যাক
+ আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।
+ XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।
+ আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।
+ আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।
+ আপনার নিমন্ত্রণপত্র, সার্ভার থেকে
+ Provisioning code-এ গরমিল আছে
+ Share বোতামটা টিপে %1$s-কে একটি আমন্ত্রপত্র পাঠান
+ পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন
+ %1$sতে এসো, আর আমার সাথে কথা বলো: %2$s
+ একটি আমন্ত্রণপত্র দেওয়া যাক...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-ca/strings.xml b/src/cheogram/res/values-ca/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..be3d17103907247a0e16a3ebff9834dba6286d21
--- /dev/null
+++ b/src/cheogram/res/values-ca/strings.xml
@@ -0,0 +1,17 @@
+
+
+ Triï el seu proveïdor de XMPP
+
+ Fer servir conversations.im
+ Crear un compte nou
+ Ja tens un compte XMPP? Aquest podria ser el cas si ja estàs usant un client XMPP diferent o has usat Converses abans. Si no, pots crear un nou compte XMPP ara mateix.\nPista: Alguns proveïdors de correu electrònic també proporcionen comptes XMPP.
+ XMPP és una xarxa de missatgeria instantània independent del proveïdor. Pots usar aquest client amb qualsevol servidor XMPP que triïs. No obstant això, per a la teva conveniència, hem fet fàcil la creació d\'un compte en Conversaciones.im¹; un proveïdor especialment adequat per a l\'ús amb Conversations.
+ Has estat convidat a %1$s. Et guiarem a través del procés de creació d\'un compte.\nEn triar%1$s com a proveïdor podràs comunicar-se amb els usuaris d\'altres proveïdors donant-los la seva adreça XMPP completa.
+ Has estat convidat a %1$s . Ja s\'ha triat un nom d\'usuari per a tu. Et guiarem en el procés de creació d\'un compte. Podràs comunicar-te amb usuaris d\'altres proveïdors donant-los la teva adreça XMPP completa.
+ La teva invitació al servidor
+ Codi d\'aprovisionament mal formatat
+ Toca el botó de compartir per a enviar al teu contacte una invitació a %1$s .
+ Si el teu contacte està a prop, també pot escanejar el codi de baix per a acceptar la teva invitació.
+ Uneix-te %1$s i xerra amb mi: %2$s
+ Comparteix la invitació amb...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-da-rDK/strings.xml b/src/cheogram/res/values-da-rDK/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a55288db910ab95e603072df6a27d6d2cd9b4d6d
--- /dev/null
+++ b/src/cheogram/res/values-da-rDK/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Vælg din XMPP-udbyder
+ Brug conversations.im
+ Opret ny konto
+ 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.
+ 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.
+ 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.
+ 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.
+ Din server invitation
+ Forkert formateret klargøringskode
+ Tryk på deleknappen for at sende din kontakt en invitation til %1$s.
+ Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation.
+ Deltag med %1$s og chat med mig: %2$s
+ Del invitation med...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-de/strings.xml b/src/cheogram/res/values-de/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e1f217512e10061b8a940f18a8dee13bae8e7773
--- /dev/null
+++ b/src/cheogram/res/values-de/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Wähle deinen XMPP-Provider
+ Benutze conversations.im
+ Neues Konto erstellen
+ Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an.
+ XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im¹ anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist.
+ Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst.
+ Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst.
+ Deine Einladung für den Server
+ Falsch formatierter Provisionierungscode
+ Tippe auf die \"Teilen\"-Schaltfläche, um deinem Kontakt eine Einladung an %1$s zu senden.
+ Wenn dein Kontakt in der Nähe ist, kann er auch den untenstehenden Code einscannen, um deine Einladung anzunehmen.
+ Komme zu %1$s und chatte mit mir: %2$s
+ Einladung teilen mit…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-el/strings.xml b/src/cheogram/res/values-el/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9ee6d96ae4fc53127a634e68087cf304cbd2a333
--- /dev/null
+++ b/src/cheogram/res/values-el/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Επιλέξτε τον πάροχο XMPP σας
+ Χρήση του conversations.im
+ Δημιουργία νέου λογαριασμού
+ Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP.
+ Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im¹, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations.
+ Έχετε προσκληθεί στο %1$s. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.
+ Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.
+ Η πρόσκλησή σας στον διακομιστή
+ Λάθος μορφοποίηση κώδικα παροχής
+ Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.
+ Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σκανάρει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.
+ Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s
+ Διαμοιρασμός πρόσκλησης με...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-es/strings.xml b/src/cheogram/res/values-es/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..690bd8f5aca3ef3793ae3c9f7c4608f90922b4de
--- /dev/null
+++ b/src/cheogram/res/values-es/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Elige tu proveedor XMPP
+ Usa conversations.im
+ Crear nueva cuenta
+ ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP.
+ XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas.\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im¹; un proveedor especializado para el uso con Conversations
+ Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa.
+ Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa.
+ Tu invitación al servidor
+ Código de abastecimiento formateado incorrectamente
+ Pulsa el botón de compartir para enviar a tu contacto una invitación a %1$s.
+ Si tu contacto está cerca, también puede escanear el código mostrado debajo para aceptar tu invitación.
+ Únete a %1$s y chatea conmigo: %2$s
+ Compartir invitación con...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-eu/strings.xml b/src/cheogram/res/values-eu/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4ab74b6abc2b43d76528d204f9c0a0443717af0d
--- /dev/null
+++ b/src/cheogram/res/values-eu/strings.xml
@@ -0,0 +1,8 @@
+
+
+ Hautatu zure XMPP hornitzailea
+ Erabili conversations.im
+ Kontu berria sortu
+ XMPP kontu bat badaukazu dagoeneko? Horrela izan daiteke beste XMPP aplikazio bat erabiltzen baduzu edo Conversations lehenago erabili baduzu. Bestela XMPP kontu berri bat sortu dezakezu oraintxe bertan.\nIradokizuna: email hornitzaile batzuek XMPP kontuak hornitzen dituzte ere.
+ XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im¹-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu.
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-fa-rIR/strings.xml b/src/cheogram/res/values-fa-rIR/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0f33625067a7892b02d89ed033b67187bc99069c
--- /dev/null
+++ b/src/cheogram/res/values-fa-rIR/strings.xml
@@ -0,0 +1,6 @@
+
+
+ لطفا سرویس دهنده پیام خود را انتخاب نمائید. برای مثال artalk.im
+ از Conversations.im استفاده کنید
+ حساب کاربری جدیدی بسازید
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-fr/strings.xml b/src/cheogram/res/values-fr/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3d7d1c4a86e74126a496be198135314531f3e88b
--- /dev/null
+++ b/src/cheogram/res/values-fr/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Choisissez votre fournisseur XMPP
+ Utiliser conversations.im
+ Créer un nouveau compte
+ Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP.
+ XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im¹ ; un fournisseur spécialement conçu pour Conversations.
+ Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.
+ Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.
+ Votre invitation au serveur
+ Code de provisionnement mal formaté
+ Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s
+ Si vos contacts sont à votre côté, ils peuvent aussi scanner le code ci dessous pour accepter votre invitation
+ Rejoignez %1$set discutez avec moi : %2$s
+ Partager une invitation avec ...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-gl/strings.xml b/src/cheogram/res/values-gl/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..636f921b67bc247f459a0331b178c57ee01c80db
--- /dev/null
+++ b/src/cheogram/res/values-gl/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Escolle o teu provedor XMPP
+ Utilizar conversations.im
+ Crear nova conta
+ Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP.
+ XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im¹; un provedor especialmente axeitado para utilizar con Conversations.
+ Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo escoller %1$s como provedor poderás comunicarte con usuarias de outros provedores cando lles deas o teu enderezo XMPP completo.
+ Convidáronte a %1$s. Escollemos un nome de usuaria por ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias de outros provedores cando lles digas o teu enderezo XMPP completo.
+ O convite do teu servidor
+ Código de aprovisionamento con formato non válido
+ Toca no botón compartir para convidar ó teu contacto a %1$s.
+ Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite.
+ Únete a %1$s e conversa conmigo: %2$s
+ Enviar convite a...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-hu/strings.xml b/src/cheogram/res/values-hu/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8e405d0e1ed4bc0ba3ee40c333c393abf48cedac
--- /dev/null
+++ b/src/cheogram/res/values-hu/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Válassza ki az XMPP szolgáltatóját
+ A conversations.im használata
+ Új fiók létrehozása
+ Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat.
+ Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im¹ szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve.
+ Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.
+ Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét.
+ Az Ön kiszolgálómeghívása
+ Helytelenül formázott kiépítési kód
+ Koppintson a megosztás gombra, hogy meghívót küldjön a partnerének erre: %1$s.
+ Ha a partnere a közelben van, akkor a meghívás elfogadásához leolvashatja a lenti kódot.
+ Csatlakozzon ehhez: %1$s, és csevegjen velem: %2$s
+ Meghívás megosztása…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-id/strings.xml b/src/cheogram/res/values-id/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fb4121cc31d9756124deddc89f0005e25bc00ad6
--- /dev/null
+++ b/src/cheogram/res/values-id/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Pilih XMPP server anda
+ Gunakan conversations.im
+ Buat akun baru
+ Anda sudah memiliki akun XMPP? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. \ NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP.
+ XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. \ NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im¹; provider yang sangat cocok digunakan dengan Conversations.
+ Anda telah diundang ke %1$s. Kami akan memandu Anda melalui proses pembuatan akun. \nSaat memilih %1$s sebagai penyedia, Anda akan dapat berkomunikasi dengan pengguna provider lain dengan memberikan alamat XMPP lengkap Anda kepada mereka.
+ Anda telah diundang ke%1$s. Username telah dipilihkan untuk Anda. Kami akan memandu Anda melalui proses pembuatan akun. \nAnda dapat berkomunikasi dengan pengguna provider lain dengan memberi mereka alamat XMPP lengkap Anda.
+ Undangan server Anda
+ Kode provisioning tidak diformat dengan benar
+ Klik tombol bagikan untuk mengirim undangan ke kontak Anda %1$s.
+ Jika kontak Anda di dekat Anda, mereka juga dapat memindai kode di bawah ini untuk menerima undangan Anda
+ Bergabung %1$s dan mengobrol dengan saya: %2$s
+ Bagikan undangan dengan...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-it/strings.xml b/src/cheogram/res/values-it/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..45800a7b8714a11fe2ae8f5a3e68c1d9a4940e85
--- /dev/null
+++ b/src/cheogram/res/values-it/strings.xml
@@ -0,0 +1,18 @@
+
+
+ Scegli il tuo provider XMPP
+ Usa conversations.im
+ Crea un nuovo account
+ Possiedi già un account XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un account XMPP adesso.
+Suggerimento: alcuni provider di email forniscono anche un account XMPP.
+ XMPP è una rete di instant messaging indipendente dal provider. Puoi usare questo client con qualsiasi server XMPP.
+In ogni caso per facilitare puoi creare facilmente un account su conversations.im, un provider pensato apposta per essere usato con Conversations.
+ Sei stato invitato su %1$s. Ti guideremo nel procedimento per creare un account.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.
+ Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.
+ Il tuo invito al server
+ Codice di approvvigionamento formattato male
+ Tocca il pulsante condividi per inviare al contatto un invito per %1$s.
+ Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito.
+ Unisciti a %1$s e chatta con me: %2$s
+ Condividi invito con...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-ja/strings.xml b/src/cheogram/res/values-ja/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2a9af807d2e8278dcedc50cc507ee81e575f780c
--- /dev/null
+++ b/src/cheogram/res/values-ja/strings.xml
@@ -0,0 +1,16 @@
+
+
+ XMPPプロバイダーを選択してください
+ conversations.imを利用する
+ アカウントを作成
+ XMPPアカウントをお持ちですか?既にほかのXMPPクライアントを利用しているか、Conversationsを利用したことがある場合はこちら。初めての方は、今すぐ新しいXMPPアカウントを作成できます。\nヒント: eメールのプロバイダーがXMPPアカウントも提供している場合があります。
+ XMPPは、プロバイダーに依存しないインスタントメッセージのプロトコルです。XMPPサーバーならどこでも、このクライアントを使用することができます。\nよろしければ、Conversationsに最適化されたプロバイダーconversations.im¹で簡単にアカウントを作成することもできます。
+ %1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。
+ %1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。
+ サーバーの招待
+ 仮コードの書式が不正です
+ 共有ボタンを叩いて、連絡先の %1$s に招待を送信する。
+ あなたの連絡先が近くにいる場合は、下のコードをスキャンして、あなたの招待を受け取ることもできます。
+ %1$s に参加して私とお話しましょう: %2$s
+ …で招待を共有
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-nl/strings.xml b/src/cheogram/res/values-nl/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a92dca5b26a90d8a15509f1a7876b56b0dbf427e
--- /dev/null
+++ b/src/cheogram/res/values-nl/strings.xml
@@ -0,0 +1,11 @@
+
+
+ Kies je XMPP-dienst
+ Conversations.im gebruiken
+ Nieuwe account registreren
+ Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan.
+ XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im¹; een provider speciaal geschikt voor Conversations.
+ Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven.
+ Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven.
+ Je server uitnodiging
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-pl/strings.xml b/src/cheogram/res/values-pl/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4e8b5d6193cdb6bb556453b93d5871d01b064397
--- /dev/null
+++ b/src/cheogram/res/values-pl/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Wybierz dostawcę XMPP
+ Użyj conversations.im
+ Stwórz nowe konto
+ Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP.
+ XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im¹; dostawcy specjalnie dostosowanego do pracy z Conversations.
+ Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP.
+ Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.
+ Zaproszenie twojego serwera
+ Niepoprawnie sformatowany kod zaopatrywania
+ Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s.
+ Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie.
+ Dołącz do %1$s aby porozmawiać ze mną: %2$s
+ Udostępnij zaproszenie...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-pt-rBR/strings.xml b/src/cheogram/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1d51c86b6d740de1efbc1fc8afcf220508315057
--- /dev/null
+++ b/src/cheogram/res/values-pt-rBR/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Selecione o seu provedor XMPP
+ Usar o conversations.im
+ Criar uma nova conta
+ Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP.
+ O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im¹, um provedor especialmente configurado para se usar com o Conversations.
+ Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.
+ Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.
+ Seu convite do servidor
+ Código de provisionamento formatado de maneira imprópria
+ Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s.
+ Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite.
+ Junte-se a %1$s e converse comigo: %2$s
+ Compartilhe o convite com...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-ro-rRO/strings.xml b/src/cheogram/res/values-ro-rRO/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..232b7d9f153222a2ba35eb5fa31651642cfe72b1
--- /dev/null
+++ b/src/cheogram/res/values-ro-rRO/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Alegeți-vă furnizorul XMPP
+ Folosește conversations.im
+ Creează un cont nou
+ Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP.
+ XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im¹; un furnizor potrivit pentru utilizarea cu aplicația Conversations.
+ Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP.
+ Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP.
+ Invitația serverului dumneavoastră
+ Cod de acces formatat necorespunzător
+ Atingeți butonul de partajare pentru a trimite contactului o invitație la %1$s.
+ Dacă e în apropiere, contactul poate scana codul de mai jos pentru a vă accepta invitația.
+ Alătură-te %1$s și discută cu mine: %2$s
+ Partajează invitația cu…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-ru/strings.xml b/src/cheogram/res/values-ru/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a8194508990ecf49fbc3fe203de2d5d039a92efb
--- /dev/null
+++ b/src/cheogram/res/values-ru/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Выберите своего XMPP-провайдера
+ Использовать conversations.im
+ Создать новый аккаунт
+ У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.\nНекоторые провайдеры электронной почты также регистрируют аккаунты XMPP.
+ XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations.
+ Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. Аккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.
+ Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. Этот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.
+ Ваше приглашение
+ Неправильный формат кода
+ Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s.
+ Если ваш контакт находится поблизости, он также может отсканировать приведенный ниже код, чтобы принять ваше приглашение.
+ Присоединяйтесь к %1$s и пообщайтесь со мной: %2$s
+ Поделиться приглашением с…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-sr/strings.xml b/src/cheogram/res/values-sr/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b9e417ed4371dcf9cf688e20b82d33bf8c61caac
--- /dev/null
+++ b/src/cheogram/res/values-sr/strings.xml
@@ -0,0 +1,9 @@
+
+
+ Одаберите вашег ИксМПП провајдера
+ Користи conversations.im
+ Направи нови налог
+ Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге.
+ ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im¹; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију
+ Ваша серверска позивница
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-sv/strings.xml b/src/cheogram/res/values-sv/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9212ad109e6208468e8615436a48a240778c0803
--- /dev/null
+++ b/src/cheogram/res/values-sv/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Använd conversations.im
+ Skapa nytt konto
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-tr-rTR/strings.xml b/src/cheogram/res/values-tr-rTR/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..43b327ef158295128f61ce5668f67cb012f8e4ec
--- /dev/null
+++ b/src/cheogram/res/values-tr-rTR/strings.xml
@@ -0,0 +1,16 @@
+
+
+ XMPP sağlayıcınızı seçin
+ conversations.im kullan
+ Yeni hesap oluştur
+ Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir.
+ XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im¹; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık.
+ %1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.
+ %1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.
+ Sunucu davetiyeniz
+ Yanlış ayarlanmış düzenleme kodu
+ Kişinize, %1$s grubuna davet etmek için Paylaş düğmesine basın.
+ Kişiniz yakınınızda ise, aşağıdaki kodu tarayak daveti kabul edebilirler.
+ %1$s grubuna katıl ve benimle sohpet et: %2$s
+ Daveti şununla paylaş...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-uk/strings.xml b/src/cheogram/res/values-uk/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f618c0cea9e76029f9aa5ad7584760affc6b9806
--- /dev/null
+++ b/src/cheogram/res/values-uk/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Виберіть постачальника послуг обміну повідомленнями XMPP
+ Скористатися conversations.im
+ Створити новий обліковий запис
+ Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP.
+ XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im¹ — у постачальника, який спеціально налаштований на роботу з цією програмою.
+ Вас запросили до %1$s. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP.
+ Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP.
+ Ваше запрошення до сервера
+ Неправильно відформатований код забезпечення
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-vi/strings.xml b/src/cheogram/res/values-vi/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ff010bd3e19b2b2afb6b662f88024909c43883f4
--- /dev/null
+++ b/src/cheogram/res/values-vi/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Chọn nhà cung cấp XMPP của bạn
+ Sử dụng conversations.im
+ Tạo tài khoản mới
+ 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.
+ 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.
+ 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.
+ 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.
+ Lời mời vào máy chủ của bạn
+ Mã cung cấp không được định dạng đúng
+ Nhấn nút chia sẻ để gửi lời mời vào %1$s đến liên hệ của bạn.
+ Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn.
+ Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s
+ Chia sẻ lời mời với...
+
\ No newline at end of file
diff --git a/src/cheogram/res/values-zh-rCN/strings.xml b/src/cheogram/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8fc58fe8fba4ae1e27104c5947a9b6bed6883ae8
--- /dev/null
+++ b/src/cheogram/res/values-zh-rCN/strings.xml
@@ -0,0 +1,16 @@
+
+
+ 选择您的XMPP提供者
+ 使用conversations.im
+ 创建新账户
+ 您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。
+ XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im¹; 特别适合与“对话”配合使用的提供商。
+ 您已受邀参加%1$s。 我们将指导您完成创建帐户的过程。\n选择%1$s作为提供者后,您可以通过提供其他人的完整XMPP地址与其他提供者的用户进行交流。
+ 您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。
+ 你的服务器邀请
+ 格式不正确的配置代码
+ 点击分享按钮向您的联系人发送加入 %1$s 的邀请。
+ 如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。
+ 加入 %1$s 和我聊天:%2$s
+ 分享邀请
+
\ No newline at end of file
diff --git a/src/cheogram/res/values/colors.xml b/src/cheogram/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d2507c765f7b808a12b151f8a190173b88b88964
--- /dev/null
+++ b/src/cheogram/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #7401cf
+ #7401cf
+
diff --git a/src/cheogram/res/values/strings.xml b/src/cheogram/res/values/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f9aaec9ee76c1d13908cbde31e32755d139e29b9
--- /dev/null
+++ b/src/cheogram/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+ Pick your XMPP provider
+ Use conversations.im
+ Create new account
+ Do you already have an XMPP account? This might be the case if you are already using a different XMPP client or have used Conversations before. If not you can create a new XMPP account right now.\nHint: Some email providers also provide XMPP accounts.
+ XMPP is a provider independent instant messaging network. You can use this client with what ever XMPP server you choose.\nHowever for your convenience we made it easy to create an account on conversations.im¹; a provider specially suited for the use with Conversations.
+ 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.
+ 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.
+ Your server invitation
+ Improperly formatted provisioning code
+ Tap the share button to send your contact an invitation to %1$s.
+ If your contact is nearby, they can also scan the code below to accept your invitation.
+ Join %1$s and chat with me: %2$s
+ Share invite with…
+
\ No newline at end of file
diff --git a/src/cheogram/res/values/themes.xml b/src/cheogram/res/values/themes.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2a07db5960caca8560cf1819520fc475600f3f4b
--- /dev/null
+++ b/src/cheogram/res/values/themes.xml
@@ -0,0 +1,414 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+