~singpolyma/cheogram-android

bba7e438799758f7cb1d144739c3ffeaa3b8c550 — Stephen Paul Weber 1 year, 10 months ago d18e0a0
Cheogram build variant with some branding
73 files changed, 3816 insertions(+), 2 deletions(-)

M .builds/debian-stable.yml
M build.gradle
A src/cheogram/AndroidManifest.xml
A src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java
A src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java
A src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
A src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java
A src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java
A src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java
A src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java
A src/cheogram/new_launcher-web.png
A src/cheogram/res/drawable-hdpi/ic_notification.png
A src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png
A src/cheogram/res/drawable-mdpi/ic_notification.png
A src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png
A src/cheogram/res/drawable-xhdpi/ic_notification.png
A src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png
A src/cheogram/res/drawable-xxhdpi/ic_notification.png
A src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png
A src/cheogram/res/drawable-xxxhdpi/ic_notification.png
A src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png
A src/cheogram/res/drawable/background.xml
A src/cheogram/res/drawable/bar_logo.xml
A src/cheogram/res/drawable/ic_launcher.xml
A src/cheogram/res/drawable/main_logo.xml
A src/cheogram/res/drawable/splash_logo.xml
A src/cheogram/res/layout/activity_easy_invite.xml
A src/cheogram/res/layout/activity_import_backup.xml
A src/cheogram/res/layout/activity_pick_server.xml
A src/cheogram/res/layout/activity_welcome.xml
A src/cheogram/res/layout/dialog_enter_password.xml
A src/cheogram/res/layout/magic_create.xml
A src/cheogram/res/menu/easy_onboarding_invite.xml
A src/cheogram/res/menu/manageaccounts.xml
A src/cheogram/res/menu/welcome_menu.xml
A src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml
A src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml
A src/cheogram/res/values-ar/strings.xml
A src/cheogram/res/values-bg/strings.xml
A src/cheogram/res/values-bn-rIN/strings.xml
A src/cheogram/res/values-ca/strings.xml
A src/cheogram/res/values-da-rDK/strings.xml
A src/cheogram/res/values-de/strings.xml
A src/cheogram/res/values-el/strings.xml
A src/cheogram/res/values-es/strings.xml
A src/cheogram/res/values-eu/strings.xml
A src/cheogram/res/values-fa-rIR/strings.xml
A src/cheogram/res/values-fr/strings.xml
A src/cheogram/res/values-gl/strings.xml
A src/cheogram/res/values-hu/strings.xml
A src/cheogram/res/values-id/strings.xml
A src/cheogram/res/values-it/strings.xml
A src/cheogram/res/values-ja/strings.xml
A src/cheogram/res/values-nl/strings.xml
A src/cheogram/res/values-pl/strings.xml
A src/cheogram/res/values-pt-rBR/strings.xml
A src/cheogram/res/values-ro-rRO/strings.xml
A src/cheogram/res/values-ru/strings.xml
A src/cheogram/res/values-sr/strings.xml
A src/cheogram/res/values-sv/strings.xml
A src/cheogram/res/values-tr-rTR/strings.xml
A src/cheogram/res/values-uk/strings.xml
A src/cheogram/res/values-vi/strings.xml
A src/cheogram/res/values-zh-rCN/strings.xml
A src/cheogram/res/values/colors.xml
A src/cheogram/res/values/strings.xml
A src/cheogram/res/values/themes.xml
M .builds/debian-stable.yml => .builds/debian-stable.yml +2 -2
@@ 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

M build.gradle => build.gradle +22 -0
@@ 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'

A src/cheogram/AndroidManifest.xml => src/cheogram/AndroidManifest.xml +81 -0
@@ 0,0 1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="eu.siacs.conversations">

    <application tools:ignore="GoogleAppIndexingWarning">
        <activity
            android:name=".ui.ManageAccountActivity"
            android:label="@string/title_activity_manage_accounts"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.WelcomeActivity"
            android:label="@string/app_name"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.PickServerActivity"
            android:label="@string/create_new_account"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.MagicCreateActivity"
            android:label="@string/create_new_account"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.EasyOnboardingInviteActivity"
            android:label="@string/invite_to_app"
            android:launchMode="singleTask" />
        <activity
            android:name=".ui.ImportBackupActivity"
            android:label="@string/restore_backup"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />

                <data android:mimeType="application/vnd.conversations.backup" />
                <data android:scheme="content" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />

                <data android:mimeType="application/vnd.conversations.backup" />
                <data android:scheme="file" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="content" />
                <data android:host="*" />
                <data android:mimeType="*/*" />
                <data android:pathPattern=".*\\.ceb" />
                <data android:pathPattern=".*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="file" />
                <data android:host="*" />
                <data android:mimeType="*/*" />
                <data android:pathPattern=".*\\.ceb" />
                <data android:pathPattern=".*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
                <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.ceb" />
            </intent-filter>
        </activity>
    </application>
</manifest>

A src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java => src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java +50 -0
@@ 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,
    }

}


A src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java => src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java +387 -0
@@ 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<OnBackupProcessed> 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<Jid> accounts = mDatabaseBackend.getAccountJids(false);
            final ArrayList<BackupFile> backupFiles = new ArrayList<>();
            final Set<String> 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<BackupFile> 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

A src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java => src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java +38 -0
@@ 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

A src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java => src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java +151 -0
@@ 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();
        });
    }
}

A src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java => src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java +242 -0
@@ 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<ImportBackupService.BackupFile> 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);
    }
}

A src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java => src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java +164 -0
@@ 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);
            }
        }
    }
}

A src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java => src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java +428 -0
@@ 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<Account> accountList = new ArrayList<>();
    protected ListView accountListView;
    protected AccountAdapter mAccountAdapter;
    protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);

    protected Pair<Integer, Intent> 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<Account> 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<Account> 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());
    }
}

A src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java => src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java +104 -0
@@ 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<Account> 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);
    }

}

A src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java => src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java +90 -0
@@ 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<Account> 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();
        }
    }
}

A src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java => src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java +234 -0
@@ 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<Account> 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());
        }
    }

}

A src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java => src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java +170 -0
@@ 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<BackupFileAdapter.BackupFileViewHolder> {

    private OnItemClickedListener listener;

    private final List<ImportBackupService.BackupFile> 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<ImportBackupService.BackupFile> 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<Jid, Void, Bitmap> {
        private final WeakReference<ImageView> 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<BitmapWorkerTask> 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

A src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java => src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java +43 -0
@@ 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<Jid> accounts = DatabaseBackend.getInstance(activity).getAccountJids(true);
        if (accounts.contains(jid)) {
            Toast.makeText(activity, R.string.account_already_exists, Toast.LENGTH_LONG).show();
            return;
        }
        final Intent serviceIntent = new Intent(activity, XmppConnectionService.class);
        serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT);
        serviceIntent.putExtra("address", jid.asBareJid().toEscapedString());
        serviceIntent.putExtra("password", accountConfiguration.password);
        Compatibility.startService(activity, serviceIntent);
        final Intent intent = new Intent(activity, EditAccountActivity.class);
        intent.putExtra("jid", jid.asBareJid().toEscapedString());
        intent.putExtra("init", true);
        activity.startActivity(intent);
    }

}

A src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java => src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java +77 -0
@@ 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

A src/cheogram/new_launcher-web.png => src/cheogram/new_launcher-web.png +0 -0
A src/cheogram/res/drawable-hdpi/ic_notification.png => src/cheogram/res/drawable-hdpi/ic_notification.png +0 -0
A src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png => src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png +0 -0
A src/cheogram/res/drawable-mdpi/ic_notification.png => src/cheogram/res/drawable-mdpi/ic_notification.png +0 -0
A src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png => src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png +0 -0
A src/cheogram/res/drawable-xhdpi/ic_notification.png => src/cheogram/res/drawable-xhdpi/ic_notification.png +0 -0
A src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png => src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png +0 -0
A src/cheogram/res/drawable-xxhdpi/ic_notification.png => src/cheogram/res/drawable-xxhdpi/ic_notification.png +0 -0
A src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png => src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png +0 -0
A src/cheogram/res/drawable-xxxhdpi/ic_notification.png => src/cheogram/res/drawable-xxxhdpi/ic_notification.png +0 -0
A src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png => src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png +0 -0
A src/cheogram/res/drawable/background.xml => src/cheogram/res/drawable/background.xml +9 -0
@@ 0,0 1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@color/splash_screen_background" />

    <item
            android:gravity="center"
            android:drawable="@drawable/splash_logo" />
</layer-list>

A src/cheogram/res/drawable/bar_logo.xml => src/cheogram/res/drawable/bar_logo.xml +20 -0
@@ 0,0 1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="35dp"
    android:height="32dp"
    android:viewportWidth="77.2339"
    android:viewportHeight="75.11203">
  <path
      android:pathData="M20.1226,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="M35.5664,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="M51.0105,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="m41.0896,0.0016c-10.1229,0.0857 -19.9788,3.6128 -27.5877,10.0052 -7.6235,6.4048 -12.4958,15.2886 -13.3632,24.9619 -0.8691,9.6922 2.3764,19.1814 8.9039,26.5442 0.8874,1.0009 1.8256,1.9519 2.8098,2.8513 -1.0988,3.9653 -3.1446,6.5406 -4.9704,8.0439 -0.7464,0.6146 -0.5833,2.063 0.375,2.19 4.0637,0.539 9.9937,0.187 14.38,-3.6702 4.0962,1.9545 8.5503,3.2572 13.1713,3.8382 10.0219,1.259 20.2894,-0.9411 28.7784,-6.2322 5.472,-3.4107 10.0164,-7.3947 13.1974,-12.568 0.861,-1.4009 0.249,-3.1938 -1.219,-3.9323l-5.7118,-2.8734c-1.4912,-0.7502 -3.2917,-0.1202 -4.2565,1.2434 -2.0854,2.9475 -4.9696,5.2758 -8.4959,7.4737 -6.0428,3.7665 -13.4811,5.3905 -20.7855,4.4727 -7.3025,-0.9175 -13.7283,-4.2768 -18.0829,-9.1887 -4.3307,-4.8848 -6.334,-10.9678 -5.7889,-17.0466 0.5468,-6.0978 3.6472,-12.0168 8.9543,-16.4755 5.3219,-4.471 12.3979,-7.0642 19.8097,-7.127 7.4114,-0.0627 14.3972,2.4092 19.5866,6.7372 1.9727,1.6453 3.6449,3.903 4.9708,6.1367 0.8507,1.4332 2.6525,2.0496 4.1419,1.2998l5.6982,-2.8683c1.471,-0.7406 2.079,-2.5335 1.265,-3.9644 -2.1418,-3.7666 -4.9307,-7.5076 -8.2277,-10.2572 -7.5755,-6.318 -17.4295,-9.6801 -27.5528,-9.5944z"
      android:fillColor="#ffffff"
      android:fillType="evenOdd"/>
</vector>

A src/cheogram/res/drawable/ic_launcher.xml => src/cheogram/res/drawable/ic_launcher.xml +57 -0
@@ 0,0 1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="512dp"
    android:height="512dp"
    android:viewportWidth="512"
    android:viewportHeight="512">
  <group>
    <clip-path
        android:pathData="M0,0l512,0l0,512l-512,0z"/>
    <path
        android:pathData="M0,0l512,0l0,512l-512,0z">
      <aapt:attr name="android:fillColor">
        <gradient 
            android:startY="0"
            android:startX="256"
            android:endY="512"
            android:endX="256"
            android:type="linear">
          <item android:offset="0" android:color="#FF6F00C7"/>
          <item android:offset="1" android:color="#FF8F00FF"/>
        </gradient>
      </aapt:attr>
    </path>
    <path
        android:pathData="M-80,271.668C246.17,314.959 276.234,79.419 311.475,-61.834l-318.796,-32.26z">
      <aapt:attr name="android:fillColor">
        <gradient 
            android:startY="-37.22124"
            android:startX="4.43356"
            android:endY="178.5528"
            android:endX="499.308"
            android:type="linear">
          <item android:offset="0" android:color="#FF8F00FF"/>
          <item android:offset="1" android:color="#008F00FF"/>
          <item android:offset="1" android:color="#FF8A01F6"/>
        </gradient>
      </aapt:attr>
    </path>
    <path
        android:pathData="m328.949,225.164c2.96,-91.795 132.235,-150.007 225.027,-84.635l151.088,136.574C568.256,449.452 277.041,776.04 206.641,703.612 118.642,613.08 108.156,437.792 199.724,409.448 291.292,381.106 326.439,303.021 328.949,225.164Z"
        android:fillColor="#8f00ff"
        android:fillAlpha="0.8"/>
    <path
        android:pathData="M194.087,255.641a18.324,18.096 90,1 0,36.193 0a18.324,18.096 90,1 0,-36.193 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M245.788,255.641a18.324,18.096 90,1 0,36.193 0a18.324,18.096 90,1 0,-36.193 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M297.489,255.641a18.324,18.096 90,1 0,36.193 0a18.324,18.096 90,1 0,-36.193 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M264.277,130.281C230.39,130.568 197.396,142.376 171.924,163.775 146.403,185.216 130.092,214.956 127.189,247.339c-2.909,32.446 7.955,64.212 29.807,88.86 2.971,3.351 6.111,6.534 9.406,9.545 -3.678,13.274 -10.527,21.896 -16.639,26.928 -2.499,2.057 -1.953,6.906 1.255,7.331 13.604,1.804 33.455,0.626 48.139,-12.287 13.713,6.543 28.623,10.904 44.093,12.849 33.55,4.215 67.922,-3.15 96.34,-20.863 18.318,-11.418 33.531,-24.755 44.18,-42.073 2.882,-4.69 0.834,-10.692 -4.081,-13.164l-19.121,-9.619c-4.992,-2.511 -11.019,-0.402 -14.249,4.162 -6.981,9.867 -16.636,17.661 -28.441,25.019 -20.229,12.609 -45.13,18.045 -69.582,14.973 -24.446,-3.071 -45.957,-14.317 -60.535,-30.76 -14.498,-16.353 -21.204,-36.716 -19.379,-57.066 1.83,-20.413 12.21,-40.228 29.976,-55.154 17.816,-14.967 41.504,-23.648 66.316,-23.859 24.811,-0.21 48.197,8.065 65.569,22.554 6.604,5.508 12.202,13.066 16.64,20.543 2.848,4.798 8.88,6.861 13.866,4.351l19.076,-9.602c4.924,-2.479 6.96,-8.481 4.235,-13.271 -7.17,-12.609 -16.506,-25.133 -27.543,-34.337 -25.36,-21.15 -58.348,-32.406 -92.237,-32.119z"
        android:fillColor="#ffffff"
        android:fillType="evenOdd"/>
  </group>
</vector>

A src/cheogram/res/drawable/main_logo.xml => src/cheogram/res/drawable/main_logo.xml +57 -0
@@ 0,0 1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="128dp"
    android:height="128dp"
    android:viewportWidth="128"
    android:viewportHeight="128">
  <group>
    <clip-path
        android:pathData="M0,0h128v128h-128z"/>
    <path
        android:pathData="M0,0h128v128h-128z">
      <aapt:attr name="android:fillColor">
        <gradient 
            android:startY="0"
            android:startX="64"
            android:endY="128"
            android:endX="64"
            android:type="linear">
          <item android:offset="0" android:color="#FF6F00C7"/>
          <item android:offset="1" android:color="#FF8F00FF"/>
        </gradient>
      </aapt:attr>
    </path>
    <path
        android:pathData="M-20,67.917C61.542,78.74 69.059,19.855 77.869,-15.459L-1.83,-23.524L-20,67.917Z">
      <aapt:attr name="android:fillColor">
        <gradient 
            android:startY="-9.30531"
            android:startX="1.10839"
            android:endY="44.6382"
            android:endX="124.827"
            android:type="linear">
          <item android:offset="0" android:color="#FF8F00FF"/>
          <item android:offset="1" android:color="#008F00FF"/>
          <item android:offset="1" android:color="#FF8A01F6"/>
        </gradient>
      </aapt:attr>
    </path>
    <path
        android:pathData="M82.237,56.291C82.977,33.342 115.296,18.789 138.494,35.132L176.266,69.276C142.064,112.363 69.26,194.01 51.66,175.903C29.66,153.27 27.039,109.448 49.931,102.362C72.823,95.277 81.61,75.755 82.237,56.291Z"
        android:fillColor="#8F00FF"
        android:fillAlpha="0.8"/>
    <path
        android:pathData="M45.136,63.37a5.406,5.474 0,1 0,10.811 0a5.406,5.474 0,1 0,-10.811 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M60.58,63.37a5.406,5.474 0,1 0,10.811 0a5.406,5.474 0,1 0,-10.811 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M76.024,63.37a5.406,5.474 0,1 0,10.811 0a5.406,5.474 0,1 0,-10.811 0z"
        android:fillColor="#ffffff"/>
    <path
        android:pathData="M66.104,25.923C55.981,26.008 46.125,29.535 38.516,35.928C30.892,42.333 26.02,51.216 25.153,60.89C24.284,70.582 27.529,80.071 34.056,87.434C34.944,88.435 35.882,89.386 36.866,90.285C35.768,94.25 33.722,96.826 31.896,98.329C31.149,98.944 31.313,100.392 32.271,100.519C36.335,101.058 42.265,100.706 46.651,96.849C50.747,98.803 55.201,100.106 59.822,100.687C69.844,101.946 80.112,99.746 88.601,94.455C94.073,91.044 98.617,87.06 101.798,81.887C102.659,80.486 102.047,78.693 100.579,77.954L94.867,75.081C93.376,74.331 91.576,74.961 90.611,76.325C88.525,79.272 85.641,81.6 82.115,83.798C76.072,87.565 68.634,89.189 61.329,88.271C54.027,87.353 47.601,83.994 43.246,79.082C38.916,74.197 36.912,68.114 37.458,62.036C38.004,55.938 41.105,50.019 46.412,45.56C51.734,41.089 58.81,38.496 66.221,38.433C73.633,38.37 80.619,40.842 85.808,45.17C87.781,46.816 89.453,49.073 90.779,51.307C91.63,52.74 93.431,53.357 94.921,52.607L100.619,49.738C102.09,48.998 102.698,47.205 101.884,45.774C99.742,42.007 96.953,38.266 93.656,35.517C86.081,29.199 76.227,25.837 66.104,25.923Z"
        android:fillColor="#ffffff"
        android:fillType="evenOdd"/>
  </group>
</vector>

A src/cheogram/res/drawable/splash_logo.xml => src/cheogram/res/drawable/splash_logo.xml +20 -0
@@ 0,0 1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="77.2339dp"
    android:height="75.11203dp"
    android:viewportWidth="77.2339"
    android:viewportHeight="75.11203">
  <path
      android:pathData="M20.1226,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="M35.5664,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="M51.0105,37.4488a5.4057,5.4738 0,1 0,10.8114 0a5.4057,5.4738 0,1 0,-10.8114 0z"
      android:fillColor="#ffffff"/>
  <path
      android:pathData="m41.0896,0.0016c-10.1229,0.0857 -19.9788,3.6128 -27.5877,10.0052 -7.6235,6.4048 -12.4958,15.2886 -13.3632,24.9619 -0.8691,9.6922 2.3764,19.1814 8.9039,26.5442 0.8874,1.0009 1.8256,1.9519 2.8098,2.8513 -1.0988,3.9653 -3.1446,6.5406 -4.9704,8.0439 -0.7464,0.6146 -0.5833,2.063 0.375,2.19 4.0637,0.539 9.9937,0.187 14.38,-3.6702 4.0962,1.9545 8.5503,3.2572 13.1713,3.8382 10.0219,1.259 20.2894,-0.9411 28.7784,-6.2322 5.472,-3.4107 10.0164,-7.3947 13.1974,-12.568 0.861,-1.4009 0.249,-3.1938 -1.219,-3.9323l-5.7118,-2.8734c-1.4912,-0.7502 -3.2917,-0.1202 -4.2565,1.2434 -2.0854,2.9475 -4.9696,5.2758 -8.4959,7.4737 -6.0428,3.7665 -13.4811,5.3905 -20.7855,4.4727 -7.3025,-0.9175 -13.7283,-4.2768 -18.0829,-9.1887 -4.3307,-4.8848 -6.334,-10.9678 -5.7889,-17.0466 0.5468,-6.0978 3.6472,-12.0168 8.9543,-16.4755 5.3219,-4.471 12.3979,-7.0642 19.8097,-7.127 7.4114,-0.0627 14.3972,2.4092 19.5866,6.7372 1.9727,1.6453 3.6449,3.903 4.9708,6.1367 0.8507,1.4332 2.6525,2.0496 4.1419,1.2998l5.6982,-2.8683c1.471,-0.7406 2.079,-2.5335 1.265,-3.9644 -2.1418,-3.7666 -4.9307,-7.5076 -8.2277,-10.2572 -7.5755,-6.318 -17.4295,-9.6801 -27.5528,-9.5944z"
      android:fillColor="#ffffff"
      android:fillType="evenOdd"/>
</vector>

A src/cheogram/res/layout/activity_easy_invite.xml => src/cheogram/res/layout/activity_easy_invite.xml +83 -0
@@ 0,0 1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">


    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="?attr/color_background_primary"
        android:orientation="vertical">

        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar" />

        <LinearLayout
            android:id="@+id/in_progress"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:visibility="gone">

            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center" />
        </LinearLayout>

        <RelativeLayout
            android:id="@+id/invite"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginLeft="@dimen/activity_horizontal_margin"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:layout_marginRight="@dimen/activity_horizontal_margin"
            android:layout_marginBottom="@dimen/activity_vertical_margin"
            android:visibility="visible">

            <TextView
                android:id="@+id/tap_to_share"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/tap_share_button_send_invite"
                android:textAppearance="@style/TextAppearance.Conversations.Body1" />

            <TextView
                android:id="@+id/scan_the_code"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/tap_to_share"
                android:layout_marginTop="24sp"
                android:text="@string/if_contact_is_nearby_use_qr"
                android:textAppearance="@style/TextAppearance.Conversations.Body1" />

            <ImageView
                android:id="@+id/qr_code"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_above="@+id/share_button"
                android:layout_below="@id/scan_the_code"
                android:layout_alignParentStart="true"
                android:layout_alignParentRight="true"
                android:layout_centerHorizontal="true"
                android:layout_margin="24sp"
                android:scaleType="fitCenter" />

            <Button
                android:id="@+id/share_button"
                style="@style/Widget.Conversations.Button.Borderless"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:minWidth="0dp"
                android:paddingLeft="16dp"
                android:paddingRight="16dp"
                android:text="@string/share"
                android:layout_centerHorizontal="true"
                android:textColor="?attr/colorAccent" />

        </RelativeLayout>

    </LinearLayout>
</layout>
\ No newline at end of file

A src/cheogram/res/layout/activity_import_backup.xml => src/cheogram/res/layout/activity_import_backup.xml +45 -0
@@ 0,0 1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">


    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="?attr/color_background_primary"
        android:orientation="vertical">

        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar" />
        <LinearLayout
            android:visibility="gone"
            android:id="@+id/in_progress"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center">
            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center" />
        </LinearLayout>



        <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:id="@+id/coordinator"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="?attr/color_background_primary">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/list"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="?attr/color_background_primary"
                android:orientation="vertical"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
        </androidx.coordinatorlayout.widget.CoordinatorLayout>

    </LinearLayout>
</layout>
\ No newline at end of file

A src/cheogram/res/layout/activity_pick_server.xml => src/cheogram/res/layout/activity_pick_server.xml +102 -0
@@ 0,0 1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include android:id="@+id/toolbar" layout="@layout/toolbar" />

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="?attr/color_background_primary">

                <LinearLayout
                    android:id="@+id/linearLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentBottom="true"
                    android:minHeight="256dp"
                    android:orientation="vertical"
                    android:paddingLeft="16dp"
                    android:paddingRight="16dp"
                    android:paddingBottom="10dp">

                    <Space
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="1" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/pick_a_server"
                        android:textAppearance="@style/TextAppearance.Conversations.Title" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="8dp"
                        android:text="@string/server_select_text"
                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />

                    <Button
                        android:id="@+id/use_cim"
                        style="@style/Widget.Conversations.Button.Borderless"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:text="@string/use_conversations.im"
                        android:textColor="?colorAccent" />

                    <Button
                        android:id="@+id/use_own_provider"
                        style="@style/Widget.Conversations.Button.Borderless"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:text="@string/use_own_provider"
                        android:textColor="?android:textColorSecondary" />
                </LinearLayout>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_above="@+id/linearLayout"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerHorizontal="true"
                        android:layout_centerVertical="true"
                        android:padding="8dp"
                        android:src="@drawable/main_logo" />
                </RelativeLayout>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentBottom="true"
                    android:layout_centerHorizontal="true"
                    android:maxLines="1"
                    android:paddingLeft="8dp"
                    android:paddingRight="8dp"
                    android:text="@string/free_for_six_month"
                    android:textColor="?android:attr/textColorSecondary"
                    android:textSize="@dimen/fineprint_size" />
            </RelativeLayout>
        </ScrollView>
    </LinearLayout>
</layout>
\ No newline at end of file

A src/cheogram/res/layout/activity_welcome.xml => src/cheogram/res/layout/activity_welcome.xml +91 -0
@@ 0,0 1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar" />

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/color_background_primary">

                <LinearLayout
                    android:id="@+id/linearLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentBottom="true"
                    android:minHeight="256dp"
                    android:orientation="vertical"
                    android:paddingLeft="16dp"
                    android:paddingRight="16dp"
                    android:paddingBottom="10dp">

                    <Space
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="1" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/welcome_header"
                        android:textAppearance="@style/TextAppearance.Conversations.Title" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="8dp"
                        android:text="@string/do_you_have_an_account"
                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />

                    <Button
                        android:id="@+id/register_new_account"
                        style="@style/Widget.Conversations.Button.Borderless"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:text="@string/create_new_account"
                        android:textColor="?colorAccent" />

                    <Button
                        android:id="@+id/use_existing"
                        style="@style/Widget.Conversations.Button.Borderless"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:text="@string/i_already_have_an_account"
                        android:textColor="?android:textColorSecondary" />
                </LinearLayout>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_above="@+id/linearLayout"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerHorizontal="true"
                        android:layout_centerVertical="true"
                        android:padding="8dp"
                        android:src="@drawable/main_logo" />
                </RelativeLayout>
            </RelativeLayout>
        </ScrollView>
    </LinearLayout>
</layout>
\ No newline at end of file

A src/cheogram/res/layout/dialog_enter_password.xml => src/cheogram/res/layout/dialog_enter_password.xml +47 -0
@@ 0,0 1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="?dialogPreferredPadding">

        <TextView
            android:id="@+id/explain"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/enter_password_to_restore"
            android:textAppearance="@style/TextAppearance.Conversations.Body2"/>

        <TextView
            android:layout_marginTop="?TextSizeBody1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/restore_warning"
            android:textAppearance="@style/TextAppearance.Conversations.Body1"/>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/account_password_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            app:passwordToggleDrawable="@drawable/visibility_toggle_drawable"
            app:passwordToggleEnabled="true"
            app:passwordToggleTint="?android:textColorSecondary"
            app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
            app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">

        <eu.siacs.conversations.ui.widget.TextInputEditText
            android:id="@+id/account_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/password"
            android:inputType="textPassword"
            android:textColor="?attr/edit_text_color"
            style="@style/Widget.Conversations.EditText"/>

        </com.google.android.material.textfield.TextInputLayout>
    </LinearLayout>
</layout>
\ No newline at end of file

A src/cheogram/res/layout/magic_create.xml => src/cheogram/res/layout/magic_create.xml +114 -0
@@ 0,0 1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include layout="@layout/toolbar" android:id="@+id/toolbar"/>

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/color_background_primary">

                <LinearLayout
                    android:id="@+id/linearLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true"
                    android:layout_alignParentBottom="true"
                    android:minHeight="256dp"
                    android:orientation="vertical"
                    android:paddingLeft="16dp"
                    android:paddingRight="16dp"
                    android:paddingBottom="10dp">

                    <Space
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="1" />

                    <TextView
                        android:id="@+id/title"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/pick_your_username"
                        android:textAppearance="@style/TextAppearance.Conversations.Title" />

                    <TextView
                        android:id="@+id/instructions"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="8dp"
                        android:text="@string/magic_create_text"
                        android:textAppearance="@style/TextAppearance.Conversations.Body1" />

                    <EditText
                        android:id="@+id/username"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:hint="@string/username_hint"
                        android:textColor="?attr/edit_text_color"
                        android:inputType="textNoSuggestions" />

                    <TextView
                        android:id="@+id/full_jid"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="8dp"
                        android:text="@string/your_full_jid_will_be"
                        android:textAppearance="@style/TextAppearance.Conversations.Caption"
                        android:visibility="invisible" />

                    <Button
                        android:id="@+id/create_account"
                        style="@style/Widget.Conversations.Button.Borderless"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="right"
                        android:text="@string/next"
                        android:textColor="?colorAccent" />
                </LinearLayout>

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_above="@+id/linearLayout"
                    android:layout_alignParentStart="true"
                    android:layout_alignParentLeft="true">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_centerHorizontal="true"
                        android:layout_centerVertical="true"
                        android:padding="8dp"
                        android:src="@drawable/main_logo" />
                </RelativeLayout>

                <TextView
                    android:id="@+id/fine_print"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentBottom="true"
                    android:layout_centerHorizontal="true"
                    android:maxLines="1"
                    android:paddingLeft="8dp"
                    android:paddingRight="8dp"
                    android:text="@string/free_for_six_month"
                    android:textColor="?android:textColorSecondary"
                    android:textSize="@dimen/fineprint_size" />
            </RelativeLayout>
        </ScrollView>
    </LinearLayout>
</layout>

A src/cheogram/res/menu/easy_onboarding_invite.xml => src/cheogram/res/menu/easy_onboarding_invite.xml +10 -0
@@ 0,0 1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">


    <item
        android:id="@+id/action_share"
        android:icon="?attr/icon_share"
        android:title="@string/invite"
        app:showAsAction="always" />
</menu>
\ No newline at end of file

A src/cheogram/res/menu/manageaccounts.xml => src/cheogram/res/menu/manageaccounts.xml +32 -0
@@ 0,0 1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
	  xmlns:app="http://schemas.android.com/apk/res-auto">

	<item
		android:id="@+id/action_add_account"
		android:icon="?attr/icon_add_person"
		app:showAsAction="always"
		android:title="@string/action_add_account"/>
	<item
		android:id="@+id/action_import_backup"
		app:showAsAction="never"
		android:title="@string/restore_backup"/>
	<item
		android:id="@+id/action_add_account_with_cert"
		app:showAsAction="never"
		android:icon="?attr/icon_add_person"
		android:title="@string/action_add_account_with_certificate"
		android:visible="true"/>
	<item
		android:id="@+id/action_enable_all"
		android:title="@string/enable_all_accounts"/>
	<item
		android:id="@+id/action_disable_all"
		android:title="@string/disable_all_accounts"/>
	<item
		android:id="@+id/action_settings"
		android:orderInCategory="100"
		app:showAsAction="never"
		android:title="@string/action_settings"/>

</menu>
\ No newline at end of file

A src/cheogram/res/menu/welcome_menu.xml => src/cheogram/res/menu/welcome_menu.xml +22 -0
@@ 0,0 1,22 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_scan_qr_code"
        android:icon="?attr/icon_scan_qr_code"
        android:orderInCategory="10"
        android:title="@string/scan_qr_code"
        android:visible="@bool/show_qr_code_scan"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/action_add_account_with_cert"
        android:title="@string/action_add_account_with_certificate"
        android:visible="true"
        app:showAsAction="never" />

    <item
        android:id="@+id/action_import_backup"
        android:title="@string/restore_backup"
        app:showAsAction="never" />
</menu>
\ No newline at end of file

A src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml => src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml +4 -0
@@ 0,0 1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <foreground android:drawable="@drawable/ic_launcher"/>
</adaptive-icon>

A src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml => src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml +4 -0
@@ 0,0 1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <foreground android:drawable="@drawable/ic_launcher"/>
</adaptive-icon>

A src/cheogram/res/values-ar/strings.xml => src/cheogram/res/values-ar/strings.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">اختر مزود خدمة XMPP الخاص بك</string>
    <string name="use_conversations.im">استخدِم conversations.im</string>
    <string name="create_new_account">أنشئ حسابًا جديدًا</string>
    </resources>
\ No newline at end of file

A src/cheogram/res/values-bg/strings.xml => src/cheogram/res/values-bg/strings.xml +17 -0
@@ 0,0 1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Изберете вашият XMPP доставчик</string>
    <string name="use_conversations.im">Използвайте conversations.im</string>
    <string name="create_new_account">Създаване не нов профил</string>
    <string name="do_you_have_an_account">Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили.
 </string>
    <string name="server_select_text">XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи добре с Conversations.</string>
    <string name="magic_create_text_on_x">Бяхте поканен(а) в %1$s. Ще Ви преведем през процеса на създаване на акаунт.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP.</string>
    <string name="magic_create_text_fixed">Бяхте поканен(а) в %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на акаунт.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP.</string>
    <string name="your_server_invitation">Вашата покана за сървъра</string>
    <string name="improperly_formatted_provisioning">Неправилно форматиран код за достъп</string>
    <string name="tap_share_button_send_invite">Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Ако контактът Ви е наблизо, може да сканира кода по-долу, за да приеме поканата Ви.</string>
    <string name="easy_invite_share_text">Присъедини се в %1$s и си пиши с мен: %2$s</string>
    <string name="share_invite_with">Споделяне на поканата чрез…</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-bn-rIN/strings.xml => src/cheogram/res/values-bn-rIN/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
    <string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
    <string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
    <string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
    <string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
    <string name="magic_create_text_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
    <string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
    <string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>
    <string name="improperly_formatted_provisioning">Provisioning code-এ গরমিল আছে</string>
    <string name="tap_share_button_send_invite">Share বোতামটা টিপে %1$s-কে একটি আমন্ত্রপত্র পাঠান</string>
    <string name="if_contact_is_nearby_use_qr">পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন</string>
    <string name="easy_invite_share_text">%1$sতে এসো, আর আমার সাথে কথা বলো: %2$s</string>
    <string name="share_invite_with">একটি আমন্ত্রণপত্র দেওয়া যাক...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-ca/strings.xml => src/cheogram/res/values-ca/strings.xml +17 -0
@@ 0,0 1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Triï el seu proveïdor de XMPP
</string>
    <string name="use_conversations.im">Fer servir conversations.im</string>
    <string name="create_new_account">Crear un compte nou</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">La teva invitació al servidor</string>
    <string name="improperly_formatted_provisioning">Codi d\'aprovisionament mal formatat</string>
    <string name="tap_share_button_send_invite">Toca el botó de compartir per a enviar al teu contacte una invitació a %1$s .</string>
    <string name="if_contact_is_nearby_use_qr">Si el teu contacte està a prop, també pot escanejar el codi de baix per a acceptar la teva invitació.</string>
    <string name="easy_invite_share_text">Uneix-te %1$s i xerra amb mi: %2$s</string>
    <string name="share_invite_with">Comparteix la invitació amb...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-da-rDK/strings.xml => src/cheogram/res/values-da-rDK/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Vælg din XMPP-udbyder</string>
    <string name="use_conversations.im">Brug conversations.im</string>
    <string name="create_new_account">Opret ny konto</string>
    <string name="do_you_have_an_account">Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti.</string>
    <string name="server_select_text">XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im¹; en udbyder, der er specielt velegnet til brug med Conversations.</string>
    <string name="magic_create_text_on_x">Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
    <string name="magic_create_text_fixed">Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse.</string>
    <string name="your_server_invitation">Din server invitation</string>
    <string name="improperly_formatted_provisioning">Forkert formateret klargøringskode</string>
    <string name="tap_share_button_send_invite">Tryk på deleknappen for at sende din kontakt en invitation til %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation.</string>
    <string name="easy_invite_share_text">Deltag med %1$s og chat med mig: %2$s</string>
    <string name="share_invite_with">Del invitation med...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-de/strings.xml => src/cheogram/res/values-de/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Wähle deinen XMPP-Provider</string>
    <string name="use_conversations.im">Benutze conversations.im</string>
    <string name="create_new_account">Neues Konto erstellen</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">Deine Einladung für den Server</string>
    <string name="improperly_formatted_provisioning">Falsch formatierter Provisionierungscode</string>
    <string name="tap_share_button_send_invite">Tippe auf die \"Teilen\"-Schaltfläche, um deinem Kontakt eine Einladung an %1$s zu senden.</string>
    <string name="if_contact_is_nearby_use_qr">Wenn dein Kontakt in der Nähe ist, kann er auch den untenstehenden Code einscannen, um deine Einladung anzunehmen.</string>
    <string name="easy_invite_share_text">Komme zu %1$s und chatte mit mir: %2$s</string>
    <string name="share_invite_with">Einladung teilen mit…</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-el/strings.xml => src/cheogram/res/values-el/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Επιλέξτε τον πάροχο XMPP σας</string>
    <string name="use_conversations.im">Χρήση του conversations.im</string>
    <string name="create_new_account">Δημιουργία νέου λογαριασμού</string>
    <string name="do_you_have_an_account">Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP.</string>
    <string name="server_select_text">Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im¹, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations.</string>
    <string name="magic_create_text_on_x">Έχετε προσκληθεί στο %1$s. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.</string>
    <string name="magic_create_text_fixed">Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας.</string>
    <string name="your_server_invitation">Η πρόσκλησή σας στον διακομιστή</string>
    <string name="improperly_formatted_provisioning">Λάθος μορφοποίηση κώδικα παροχής</string>
    <string name="tap_share_button_send_invite">Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σκανάρει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας.</string>
    <string name="easy_invite_share_text">Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s</string>
    <string name="share_invite_with">Διαμοιρασμός πρόσκλησης με...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-es/strings.xml => src/cheogram/res/values-es/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Elige tu proveedor XMPP</string>
    <string name="use_conversations.im">Usa conversations.im</string>
    <string name="create_new_account">Crear nueva cuenta</string>
    <string name="do_you_have_an_account">¿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.</string>
    <string name="server_select_text">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 </string>
    <string name="magic_create_text_on_x">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. </string>
    <string name="magic_create_text_fixed">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. </string>
    <string name="your_server_invitation">Tu invitación al servidor</string>
    <string name="improperly_formatted_provisioning">Código de abastecimiento formateado incorrectamente</string>
    <string name="tap_share_button_send_invite">Pulsa el botón de compartir para enviar a tu contacto una invitación a %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Si tu contacto está cerca, también puede escanear el código mostrado debajo para aceptar tu invitación.</string>
    <string name="easy_invite_share_text">Únete a %1$s y chatea conmigo: %2$s</string>
    <string name="share_invite_with">Compartir invitación con...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-eu/strings.xml => src/cheogram/res/values-eu/strings.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Hautatu zure XMPP hornitzailea</string>
    <string name="use_conversations.im">Erabili conversations.im</string>
    <string name="create_new_account">Kontu berria sortu</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    </resources>
\ No newline at end of file

A src/cheogram/res/values-fa-rIR/strings.xml => src/cheogram/res/values-fa-rIR/strings.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">لطفا سرویس دهنده پیام خود را انتخاب نمائید. برای مثال artalk.im</string>
    <string name="use_conversations.im">از Conversations.im استفاده کنید</string>
    <string name="create_new_account">حساب کاربری جدیدی بسازید</string>
    </resources>
\ No newline at end of file

A src/cheogram/res/values-fr/strings.xml => src/cheogram/res/values-fr/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Choisissez votre fournisseur XMPP</string>
    <string name="use_conversations.im">Utiliser conversations.im</string>
    <string name="create_new_account">Créer un nouveau compte</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">Votre invitation au serveur</string>
    <string name="improperly_formatted_provisioning">Code de provisionnement mal formaté</string>
    <string name="tap_share_button_send_invite">Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s</string>
    <string name="if_contact_is_nearby_use_qr">Si vos contacts sont à votre côté, ils peuvent aussi scanner le code ci dessous pour accepter votre invitation</string>
    <string name="easy_invite_share_text">Rejoignez %1$set discutez avec moi : %2$s</string>
    <string name="share_invite_with">Partager une invitation avec ...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-gl/strings.xml => src/cheogram/res/values-gl/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Escolle o teu provedor XMPP</string>
    <string name="use_conversations.im">Utilizar conversations.im</string>
    <string name="create_new_account">Crear nova conta</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">O convite do teu servidor</string>
    <string name="improperly_formatted_provisioning">Código de aprovisionamento con formato non válido</string>
    <string name="tap_share_button_send_invite">Toca no botón compartir para convidar ó teu contacto a %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite.</string>
    <string name="easy_invite_share_text">Únete a %1$s e conversa conmigo: %2$s</string>
    <string name="share_invite_with">Enviar convite a...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-hu/strings.xml => src/cheogram/res/values-hu/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Válassza ki az XMPP szolgáltatóját</string>
    <string name="use_conversations.im">A conversations.im használata</string>
    <string name="create_new_account">Új fiók létrehozása</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">Az Ön kiszolgálómeghívása</string>
    <string name="improperly_formatted_provisioning">Helytelenül formázott kiépítési kód</string>
    <string name="tap_share_button_send_invite">Koppintson a megosztás gombra, hogy meghívót küldjön a partnerének erre: %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Ha a partnere a közelben van, akkor a meghívás elfogadásához leolvashatja a lenti kódot.</string>
    <string name="easy_invite_share_text">Csatlakozzon ehhez: %1$s, és csevegjen velem: %2$s</string>
    <string name="share_invite_with">Meghívás megosztása…</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-id/strings.xml => src/cheogram/res/values-id/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Pilih XMPP server anda</string>
    <string name="use_conversations.im">Gunakan conversations.im</string>
    <string name="create_new_account">Buat akun baru</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">Undangan server Anda</string>
    <string name="improperly_formatted_provisioning">Kode provisioning tidak diformat dengan benar</string>
    <string name="tap_share_button_send_invite">Klik tombol bagikan untuk mengirim undangan ke kontak Anda %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Jika kontak Anda di dekat Anda, mereka juga dapat memindai kode di bawah ini untuk menerima undangan Anda</string>
    <string name="easy_invite_share_text">Bergabung %1$s dan mengobrol dengan saya: %2$s</string>
    <string name="share_invite_with">Bagikan undangan dengan...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-it/strings.xml => src/cheogram/res/values-it/strings.xml +18 -0
@@ 0,0 1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">Scegli il tuo provider XMPP</string>
    <string name="use_conversations.im">Usa conversations.im</string>
    <string name="create_new_account">Crea un nuovo account</string>
    <string name="do_you_have_an_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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>
    <string name="magic_create_text_fixed">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.</string>
    <string name="your_server_invitation">Il tuo invito al server</string>
    <string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
    <string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito.</string>
    <string name="easy_invite_share_text">Unisciti a %1$s e chatta con me: %2$s</string>
    <string name="share_invite_with">Condividi invito con...</string>
</resources>
\ No newline at end of file

A src/cheogram/res/values-ja/strings.xml => src/cheogram/res/values-ja/strings.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pick_a_server">XMPPプロバイダーを選択してください</string>
    <string name="use_conversations.im">conversations.imを利用する</string>
    <string name="create_new_account">アカウントを作成</string>
    <string name="do_you_have_an_account">XMPPアカウントをお持ちですか?既にほかのXMPPクライアントを利用しているか、Conversationsを利用したことがある場合はこちら。初めての方は、今すぐ新しいXMPPアカウントを作成できます。\nヒント: eメールのプロバイダーがXMPPアカウントも提供している場合があります。</string>
    <string name="server_select_text">XMPPは、プロバイダーに依存しないインスタントメッセージのプロトコルです。XMPPサーバーならどこでも、このクライアントを使用することができます。\nよろしければ、Conversationsに最適化されたプロバイダーconversations.im¹で簡単にアカウントを作成することもできます。</string>
    <string name="magic_create_text_on_x">%1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
    <string name="magic_create_text_fixed">%1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。</string>
    <string name="your_server_invitation">サーバーの招待</string>
    <string name="improperly_formatted_provisioning">仮コードの書式が不正です</string>
    <string name="tap_share_button_send_invite">共有ボタンを叩いて、連絡先の %1$s に招待を送信する。</string>
    <string name="if_contact_is_nearby_use_qr">あなたの連絡先が近くにいる場合は、下のコードをスキャンして、あなたの招待を受け取ることもできます。</string>
    <string name="easy_invite_share_text">%1$s に参加して私とお話しましょう: %2$s</string>