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 => +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 => +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 => +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 =>