From bba7e438799758f7cb1d144739c3ffeaa3b8c550 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 12 Jul 2021 09:31:57 -0500 Subject: [PATCH] Cheogram build variant with some branding --- .builds/debian-stable.yml | 4 +- build.gradle | 22 + src/cheogram/AndroidManifest.xml | 81 ++++ .../entities/AccountConfiguration.java | 50 ++ .../services/ImportBackupService.java | 387 ++++++++++++++++ .../services/QuickConversationsService.java | 38 ++ .../ui/EasyOnboardingInviteActivity.java | 151 ++++++ .../ui/ImportBackupActivity.java | 242 ++++++++++ .../conversations/ui/MagicCreateActivity.java | 164 +++++++ .../ui/ManageAccountActivity.java | 428 ++++++++++++++++++ .../conversations/ui/PickServerActivity.java | 104 +++++ .../ui/ShareViaAccountActivity.java | 90 ++++ .../conversations/ui/WelcomeActivity.java | 234 ++++++++++ .../ui/adapter/BackupFileAdapter.java | 170 +++++++ .../utils/ProvisioningUtils.java | 43 ++ .../conversations/utils/SignupUtils.java | 77 ++++ src/cheogram/new_launcher-web.png | Bin 0 -> 19483 bytes .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 798 bytes .../drawable-hdpi/ic_unarchive_white_24dp.png | Bin 0 -> 258 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 554 bytes .../drawable-mdpi/ic_unarchive_white_24dp.png | Bin 0 -> 181 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1075 bytes .../ic_unarchive_white_24dp.png | Bin 0 -> 273 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1619 bytes .../ic_unarchive_white_24dp.png | Bin 0 -> 391 bytes .../res/drawable-xxxhdpi/ic_notification.png | Bin 0 -> 2117 bytes .../ic_unarchive_white_24dp.png | Bin 0 -> 503 bytes src/cheogram/res/drawable/background.xml | 9 + src/cheogram/res/drawable/bar_logo.xml | 20 + src/cheogram/res/drawable/ic_launcher.xml | 57 +++ src/cheogram/res/drawable/main_logo.xml | 57 +++ src/cheogram/res/drawable/splash_logo.xml | 20 + .../res/layout/activity_easy_invite.xml | 83 ++++ .../res/layout/activity_import_backup.xml | 45 ++ .../res/layout/activity_pick_server.xml | 102 +++++ src/cheogram/res/layout/activity_welcome.xml | 91 ++++ .../res/layout/dialog_enter_password.xml | 47 ++ src/cheogram/res/layout/magic_create.xml | 114 +++++ .../res/menu/easy_onboarding_invite.xml | 10 + src/cheogram/res/menu/manageaccounts.xml | 32 ++ src/cheogram/res/menu/welcome_menu.xml | 22 + .../res/mipmap-anydpi-v26/new_launcher.xml | 4 + .../mipmap-anydpi-v26/new_launcher_round.xml | 4 + src/cheogram/res/values-ar/strings.xml | 6 + src/cheogram/res/values-bg/strings.xml | 17 + src/cheogram/res/values-bn-rIN/strings.xml | 16 + src/cheogram/res/values-ca/strings.xml | 17 + src/cheogram/res/values-da-rDK/strings.xml | 16 + src/cheogram/res/values-de/strings.xml | 16 + src/cheogram/res/values-el/strings.xml | 16 + src/cheogram/res/values-es/strings.xml | 16 + src/cheogram/res/values-eu/strings.xml | 8 + src/cheogram/res/values-fa-rIR/strings.xml | 6 + src/cheogram/res/values-fr/strings.xml | 16 + src/cheogram/res/values-gl/strings.xml | 16 + src/cheogram/res/values-hu/strings.xml | 16 + src/cheogram/res/values-id/strings.xml | 16 + src/cheogram/res/values-it/strings.xml | 18 + src/cheogram/res/values-ja/strings.xml | 16 + src/cheogram/res/values-nl/strings.xml | 11 + src/cheogram/res/values-pl/strings.xml | 16 + src/cheogram/res/values-pt-rBR/strings.xml | 16 + src/cheogram/res/values-ro-rRO/strings.xml | 16 + src/cheogram/res/values-ru/strings.xml | 16 + src/cheogram/res/values-sr/strings.xml | 9 + src/cheogram/res/values-sv/strings.xml | 5 + src/cheogram/res/values-tr-rTR/strings.xml | 16 + src/cheogram/res/values-uk/strings.xml | 12 + src/cheogram/res/values-vi/strings.xml | 16 + src/cheogram/res/values-zh-rCN/strings.xml | 16 + src/cheogram/res/values/colors.xml | 5 + src/cheogram/res/values/strings.xml | 16 + src/cheogram/res/values/themes.xml | 414 +++++++++++++++++ 73 files changed, 3816 insertions(+), 2 deletions(-) create mode 100644 src/cheogram/AndroidManifest.xml create mode 100644 src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java create mode 100644 src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java create mode 100644 src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java create mode 100644 src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java create mode 100644 src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java create mode 100644 src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java create mode 100644 src/cheogram/new_launcher-web.png create mode 100644 src/cheogram/res/drawable-hdpi/ic_notification.png create mode 100644 src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png create mode 100644 src/cheogram/res/drawable-mdpi/ic_notification.png create mode 100644 src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png create mode 100644 src/cheogram/res/drawable-xhdpi/ic_notification.png create mode 100644 src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png create mode 100644 src/cheogram/res/drawable-xxhdpi/ic_notification.png create mode 100644 src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png create mode 100644 src/cheogram/res/drawable-xxxhdpi/ic_notification.png create mode 100644 src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png create mode 100644 src/cheogram/res/drawable/background.xml create mode 100644 src/cheogram/res/drawable/bar_logo.xml create mode 100644 src/cheogram/res/drawable/ic_launcher.xml create mode 100644 src/cheogram/res/drawable/main_logo.xml create mode 100644 src/cheogram/res/drawable/splash_logo.xml create mode 100644 src/cheogram/res/layout/activity_easy_invite.xml create mode 100644 src/cheogram/res/layout/activity_import_backup.xml create mode 100644 src/cheogram/res/layout/activity_pick_server.xml create mode 100644 src/cheogram/res/layout/activity_welcome.xml create mode 100644 src/cheogram/res/layout/dialog_enter_password.xml create mode 100644 src/cheogram/res/layout/magic_create.xml create mode 100644 src/cheogram/res/menu/easy_onboarding_invite.xml create mode 100644 src/cheogram/res/menu/manageaccounts.xml create mode 100644 src/cheogram/res/menu/welcome_menu.xml create mode 100644 src/cheogram/res/mipmap-anydpi-v26/new_launcher.xml create mode 100644 src/cheogram/res/mipmap-anydpi-v26/new_launcher_round.xml create mode 100644 src/cheogram/res/values-ar/strings.xml create mode 100644 src/cheogram/res/values-bg/strings.xml create mode 100644 src/cheogram/res/values-bn-rIN/strings.xml create mode 100644 src/cheogram/res/values-ca/strings.xml create mode 100644 src/cheogram/res/values-da-rDK/strings.xml create mode 100644 src/cheogram/res/values-de/strings.xml create mode 100644 src/cheogram/res/values-el/strings.xml create mode 100644 src/cheogram/res/values-es/strings.xml create mode 100644 src/cheogram/res/values-eu/strings.xml create mode 100644 src/cheogram/res/values-fa-rIR/strings.xml create mode 100644 src/cheogram/res/values-fr/strings.xml create mode 100644 src/cheogram/res/values-gl/strings.xml create mode 100644 src/cheogram/res/values-hu/strings.xml create mode 100644 src/cheogram/res/values-id/strings.xml create mode 100644 src/cheogram/res/values-it/strings.xml create mode 100644 src/cheogram/res/values-ja/strings.xml create mode 100644 src/cheogram/res/values-nl/strings.xml create mode 100644 src/cheogram/res/values-pl/strings.xml create mode 100644 src/cheogram/res/values-pt-rBR/strings.xml create mode 100644 src/cheogram/res/values-ro-rRO/strings.xml create mode 100644 src/cheogram/res/values-ru/strings.xml create mode 100644 src/cheogram/res/values-sr/strings.xml create mode 100644 src/cheogram/res/values-sv/strings.xml create mode 100644 src/cheogram/res/values-tr-rTR/strings.xml create mode 100644 src/cheogram/res/values-uk/strings.xml create mode 100644 src/cheogram/res/values-vi/strings.xml create mode 100644 src/cheogram/res/values-zh-rCN/strings.xml create mode 100644 src/cheogram/res/values/colors.xml create mode 100644 src/cheogram/res/values/strings.xml create mode 100644 src/cheogram/res/values/themes.xml diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml index 1697e401a91b5584bdfb96bcc62406e11a32dbe7..299f65672c4201fda1f83b0e40aadf1b8bd67f4a 100644 --- a/.builds/debian-stable.yml +++ b/.builds/debian-stable.yml @@ -22,6 +22,6 @@ tasks: yes | android/cmdline-tools/tools/bin/sdkmanager --licenses - build: | cd cheogram-android - ./gradlew assembleConversationsFreeCompatDebug + ./gradlew assembleCheogramFreeSystemDebug - assets: | - mv cheogram-android/build/outputs/apk/conversationsFreeCompat/debug/*.apk cheogram.apk + mv cheogram-android/build/outputs/apk/cheogramFreeSystem/debug/*.apk cheogram.apk diff --git a/build.gradle b/build.gradle index 9adcfd56692cb4df268bfbebfbb92295bbdb1d3e..78285540ecd45732a3d8a91fef2ac8ca1630c2b6 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,17 @@ android { dimension "mode" } + cheogram { + dimension "mode" + + applicationId = "com.cheogram.android" + resValue "string", "applicationId", applicationId + + def appName = "Cheogram" + resValue "string", "app_name", appName + buildConfigField "String", "APP_NAME", "\"$appName\""; + } + playstore { dimension "distribution" versionNameSuffix "+p" @@ -200,6 +211,17 @@ android { srcDir 'src/conversationsFree/java' } } + cheogramFreeCompat { + java { + srcDir 'src/freeCompat/java' + srcDir 'src/conversationsFree/java' + } + } + cheogramFreeSystem { + java { + srcDir 'src/conversationsFree/java' + } + } conversationsPlaystoreCompat { java { srcDir 'src/playstoreCompat/java' diff --git a/src/cheogram/AndroidManifest.xml b/src/cheogram/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..62396bed1af27fcdcac16db152115fbd362223ec --- /dev/null +++ b/src/cheogram/AndroidManifest.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..65b6804f9b6e7bb4abac560824bf5958a38dbd0b --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java @@ -0,0 +1,50 @@ +package eu.siacs.conversations.entities; + +import com.google.common.base.Preconditions; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +import eu.siacs.conversations.xmpp.Jid; + +public class AccountConfiguration { + + private static final Gson GSON = new GsonBuilder().create(); + + public Protocol protocol; + public String address; + public String password; + + public Jid getJid() { + return Jid.ofEscaped(address); + } + + public static AccountConfiguration parse(final String input) { + final AccountConfiguration c; + try { + c = GSON.fromJson(input, AccountConfiguration.class); + } catch (JsonSyntaxException e) { + throw new IllegalArgumentException("Not a valid JSON string", e); + } + Preconditions.checkArgument( + c.protocol == Protocol.XMPP, + "Protocol must be XMPP" + ); + Preconditions.checkArgument( + c.address != null && c.getJid().isBareJid() && !c.getJid().isDomainJid(), + "Invalid XMPP address" + ); + Preconditions.checkArgument( + c.password != null && c.password.length() > 0, + "No password specified" + ); + return c; + } + + public enum Protocol { + @SerializedName("xmpp") XMPP, + } + +} + diff --git a/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java new file mode 100644 index 0000000000000000000000000000000000000000..9c6ebaafd08e438e48f371d3a0a3c6f61714138e --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java @@ -0,0 +1,387 @@ +package eu.siacs.conversations.services; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.provider.OpenableColumns; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.google.common.base.Charsets; +import com.google.common.base.Stopwatch; +import com.google.common.io.CountingInputStream; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipException; + +import javax.crypto.BadPaddingException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.xmpp.Jid; + +public class ImportBackupService extends Service { + + private static final int NOTIFICATION_ID = 21; + private static final AtomicBoolean running = new AtomicBoolean(false); + private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); + private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName()); + private final Set mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>()); + private DatabaseBackend mDatabaseBackend; + private NotificationManager notificationManager; + + private static int count(String input, char c) { + int count = 0; + for (char aChar : input.toCharArray()) { + if (aChar == c) { + ++count; + } + } + return count; + } + + @Override + public void onCreate() { + mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); + notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + final String password = intent.getStringExtra("password"); + final Uri data = intent.getData(); + final Uri uri; + if (data == null) { + final String file = intent.getStringExtra("file"); + uri = file == null ? null : Uri.fromFile(new File(file)); + } else { + uri = data; + } + + if (password == null || password.isEmpty() || uri == null) { + return START_NOT_STICKY; + } + if (running.compareAndSet(false, true)) { + executor.execute(() -> { + startForegroundService(); + final boolean success = importBackup(uri, password); + stopForeground(true); + running.set(false); + if (success) { + notifySuccess(); + } + stopSelf(); + }); + } else { + Log.d(Config.LOGTAG, "backup already running"); + } + return START_NOT_STICKY; + } + + public boolean getLoadingState() { + return running.get(); + } + + public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { + executor.execute(() -> { + final List accounts = mDatabaseBackend.getAccountJids(false); + final ArrayList backupFiles = new ArrayList<>(); + final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name))); + for (String app : apps) { + final File directory = new File(FileBackend.getBackupDirectory(app)); + if (!directory.exists() || !directory.isDirectory()) { + Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); + continue; + } + final File[] files = directory.listFiles(); + if (files == null) { + onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); + return; + } + for (final File file : files) { + if (file.isFile() && file.getName().endsWith(".ceb")) { + try { + final BackupFile backupFile = BackupFile.read(file); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (IOException | IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } + } + } + } + Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString())); + onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); + }); + } + + private void startForegroundService() { + startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0)); + } + + private void updateImportBackupNotification(final long total, final long current) { + final int max; + final int progress; + if (total == 0) { + max = 1; + progress = 0; + } else { + max = 100; + progress = (int) (current * 100 / total); + } + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + try { + notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress)); + } catch (final RuntimeException e) { + Log.d(Config.LOGTAG, "unable to make notification", e); + } + } + + private Notification createImportBackupNotification(final int max, final int progress) { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + mBuilder.setContentTitle(getString(R.string.restoring_backup)) + .setSmallIcon(R.drawable.ic_unarchive_white_24dp) + .setProgress(max, progress, max == 1 && progress == 0); + return mBuilder.build(); + } + + private boolean importBackup(final Uri uri, final String password) { + Log.d(Config.LOGTAG, "importing backup from " + uri); + final Stopwatch stopwatch = Stopwatch.createStarted(); + try { + final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); + final InputStream inputStream; + final String path = uri.getPath(); + final long fileSize; + if ("file".equals(uri.getScheme()) && path != null) { + final File file = new File(path); + inputStream = new FileInputStream(file); + fileSize = file.length(); + } else { + final Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); + if (returnCursor == null) { + fileSize = 0; + } else { + returnCursor.moveToFirst(); + fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE)); + returnCursor.close(); + } + inputStream = getContentResolver().openInputStream(uri); + } + if (inputStream == null) { + synchronized (mOnBackupProcessedListeners) { + for (final OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onBackupRestoreFailed(); + } + } + return false; + } + final CountingInputStream countingInputStream = new CountingInputStream(inputStream); + final DataInputStream dataInputStream = new DataInputStream(countingInputStream); + final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + Log.d(Config.LOGTAG, backupFileHeader.toString()); + + if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onAccountAlreadySetup(); + } + } + return false; + } + + final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt()); + + final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); + final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher); + + final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); + final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); + db.beginTransaction(); + String line; + StringBuilder multiLineQuery = null; + while ((line = reader.readLine()) != null) { + int count = count(line, '\''); + if (multiLineQuery != null) { + multiLineQuery.append('\n'); + multiLineQuery.append(line); + if (count % 2 == 1) { + db.execSQL(multiLineQuery.toString()); + multiLineQuery = null; + updateImportBackupNotification(fileSize, countingInputStream.getCount()); + } + } else { + if (count % 2 == 0) { + db.execSQL(line); + updateImportBackupNotification(fileSize, countingInputStream.getCount()); + } else { + multiLineQuery = new StringBuilder(line); + } + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + final Jid jid = backupFileHeader.getJid(); + final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()}); + countCursor.moveToFirst(); + final int count = countCursor.getInt(0); + Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString())); + countCursor.close(); + stopBackgroundService(); + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onBackupRestored(); + } + } + return true; + } catch (final Exception e) { + final Throwable throwable = e.getCause(); + final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException; + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + if (reasonWasCrypto) { + l.onBackupDecryptionFailed(); + } else { + l.onBackupRestoreFailed(); + } + } + } + Log.d(Config.LOGTAG, "error restoring backup " + uri, e); + return false; + } + } + + private void notifySuccess() { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) + .setContentText(getString(R.string.notification_restored_backup_subtitle)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) + .setSmallIcon(R.drawable.ic_unarchive_white_24dp); + notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + } + + private void stopBackgroundService() { + Intent intent = new Intent(this, XmppConnectionService.class); + stopService(intent); + } + + public void removeOnBackupProcessedListener(OnBackupProcessed listener) { + synchronized (mOnBackupProcessedListeners) { + mOnBackupProcessedListeners.remove(listener); + } + } + + public void addOnBackupProcessedListener(OnBackupProcessed listener) { + synchronized (mOnBackupProcessedListeners) { + mOnBackupProcessedListeners.add(listener); + } + } + + @Override + public IBinder onBind(Intent intent) { + return this.binder; + } + + public interface OnBackupFilesLoaded { + void onBackupFilesLoaded(List files); + } + + public interface OnBackupProcessed { + void onBackupRestored(); + + void onBackupDecryptionFailed(); + + void onBackupRestoreFailed(); + + void onAccountAlreadySetup(); + } + + public static class BackupFile { + private final Uri uri; + private final BackupFileHeader header; + + private BackupFile(Uri uri, BackupFileHeader header) { + this.uri = uri; + this.header = header; + } + + private static BackupFile read(File file) throws IOException { + final FileInputStream fileInputStream = new FileInputStream(file); + final DataInputStream dataInputStream = new DataInputStream(fileInputStream); + BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + fileInputStream.close(); + return new BackupFile(Uri.fromFile(file), backupFileHeader); + } + + public static BackupFile read(final Context context, final Uri uri) throws IOException { + final InputStream inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + throw new FileNotFoundException(); + } + final DataInputStream dataInputStream = new DataInputStream(inputStream); + BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + inputStream.close(); + return new BackupFile(uri, backupFileHeader); + } + + public BackupFileHeader getHeader() { + return header; + } + + public Uri getUri() { + return uri; + } + } + + public class ImportBackupServiceBinder extends Binder { + public ImportBackupService getService() { + return ImportBackupService.this; + } + } +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java new file mode 100644 index 0000000000000000000000000000000000000000..b2a0d17f4acc8caa60e316cc64217e3a3604c9b7 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -0,0 +1,38 @@ +package eu.siacs.conversations.services; + +import android.content.Intent; +import android.util.Log; + +import eu.siacs.conversations.Config; + +public class QuickConversationsService extends AbstractQuickConversationsService { + + QuickConversationsService(XmppConnectionService xmppConnectionService) { + super(xmppConnectionService); + } + + @Override + public void considerSync() { + + } + + @Override + public void signalAccountStateChange() { + + } + + @Override + public boolean isSynchronizing() { + return false; + } + + @Override + public void considerSyncBackground(boolean force) { + + } + + @Override + public void handleSmsReceived(Intent intent) { + Log.d(Config.LOGTAG,"ignoring received SMS"); + } +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..fea92401b76e511d0371e91c173114dc9000426b --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java @@ -0,0 +1,151 @@ +package eu.siacs.conversations.ui; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import androidx.databinding.DataBindingUtil; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityEasyInviteBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.utils.EasyOnboardingInvite; +import eu.siacs.conversations.xmpp.Jid; + +public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested { + + private ActivityEasyInviteBinding binding; + + private EasyOnboardingInvite easyOnboardingInvite; + + + @Override + public void onCreate(final Bundle bundle) { + super.onCreate(bundle); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar(), true); + this.binding.shareButton.setOnClickListener(v -> share()); + if (bundle != null && bundle.containsKey("invite")) { + this.easyOnboardingInvite = bundle.getParcelable("invite"); + if (this.easyOnboardingInvite != null) { + showInvite(this.easyOnboardingInvite); + return; + } + } + this.showLoading(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu); + final MenuItem share = menu.findItem(R.id.action_share); + share.setVisible(easyOnboardingInvite != null); + return super.onCreateOptionsMenu(menu); + } + + public boolean onOptionsItemSelected(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.action_share) { + share(); + return true; + } else { + return super.onOptionsItemSelected(menuItem); + } + } + + private void share() { + final String shareText = getString( + R.string.easy_invite_share_text, + easyOnboardingInvite.getDomain(), + easyOnboardingInvite.getShareableLink() + ); + final Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with))); + } + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + if (easyOnboardingInvite != null) { + showInvite(easyOnboardingInvite); + } else { + showLoading(); + } + } + + private void showLoading() { + this.binding.inProgress.setVisibility(View.VISIBLE); + this.binding.invite.setVisibility(View.GONE); + } + + private void showInvite(final EasyOnboardingInvite invite) { + this.binding.inProgress.setVisibility(View.GONE); + this.binding.invite.setVisibility(View.VISIBLE); + this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain())); + final Point size = new Point(); + getWindowManager().getDefaultDisplay().getSize(size); + final int width = Math.min(size.x, size.y); + final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width); + binding.qrCode.setImageBitmap(bitmap); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + if (easyOnboardingInvite != null) { + bundle.putParcelable("invite", easyOnboardingInvite); + } + } + + @Override + void onBackendConnected() { + if (easyOnboardingInvite != null) { + return; + } + final Intent launchIntent = getIntent(); + final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT); + final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra); + if (jid == null) { + return; + } + final Account account = xmppConnectionService.findAccountByJid(jid); + xmppConnectionService.requestEasyOnboardingInvite(account, this); + } + + public static void launch(final Account account, final Activity context) { + final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + context.startActivity(intent); + } + + @Override + public void inviteRequested(EasyOnboardingInvite invite) { + this.easyOnboardingInvite = invite; + Log.d(Config.LOGTAG, "invite requested"); + refreshUi(); + } + + @Override + public void inviteRequestFailed(final String message) { + runOnUiThread(() -> { + if (!Strings.isNullOrEmpty(message)) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } + finish(); + }); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..a1f4d8d11a37ce20132442579bcfcd8fdb50c60a --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -0,0 +1,242 @@ +package eu.siacs.conversations.ui; + +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.databinding.DataBindingUtil; + +import com.google.android.material.snackbar.Snackbar; + +import java.io.IOException; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityImportBackupBinding; +import eu.siacs.conversations.databinding.DialogEnterPasswordBinding; +import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.ui.adapter.BackupFileAdapter; +import eu.siacs.conversations.utils.ThemeHelper; + +public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { + + private ActivityImportBackupBinding binding; + + private BackupFileAdapter backupFileAdapter; + private ImportBackupService service; + + private boolean mLoadingState = false; + + private int mTheme; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + this.mTheme = ThemeHelper.find(this); + setTheme(this.mTheme); + super.onCreate(savedInstanceState); + binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup); + setSupportActionBar(binding.toolbar); + setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false)); + this.backupFileAdapter = new BackupFileAdapter(); + this.binding.list.setAdapter(this.backupFileAdapter); + this.backupFileAdapter.setOnItemClickedListener(this); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.import_backup, menu); + final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file); + openBackup.setVisible(!this.mLoadingState); + return true; + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putBoolean("loading_state", this.mLoadingState); + super.onSaveInstanceState(bundle); + } + + @Override + public void onStart() { + super.onStart(); + final int theme = ThemeHelper.find(this); + if (this.mTheme != theme) { + recreate(); + } else { + bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); + } + final Intent intent = getIntent(); + if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) { + Uri uri = intent.getData(); + if (uri != null) { + openBackupFileFromUri(uri, true); + } + } + } + + @Override + public void onStop() { + super.onStop(); + if (this.service != null) { + this.service.removeOnBackupProcessedListener(this); + } + unbindService(this); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service; + this.service = binder.getService(); + this.service.addOnBackupProcessedListener(this); + setLoadingState(this.service.getLoadingState()); + this.service.loadBackupFiles(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + this.service = null; + } + + @Override + public void onBackupFilesLoaded(final List files) { + runOnUiThread(() -> backupFileAdapter.setFiles(files)); + } + + @Override + public void onClick(final ImportBackupService.BackupFile backupFile) { + showEnterPasswordDialog(backupFile, false); + } + + private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) { + try { + final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); + showEnterPasswordDialog(backupFile, finishOnCancel); + } catch (final IOException | IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); + Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); + } + } + + private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) { + final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); + Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri()); + enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString())); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setView(enterPasswordBinding.getRoot()); + builder.setTitle(R.string.enter_password); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> { + if (finishOnCancel) { + finish(); + } + }); + builder.setPositiveButton(R.string.restore, null); + builder.setCancelable(false); + final AlertDialog dialog = builder.create(); + dialog.setOnShowListener((d) -> { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { + final String password = enterPasswordBinding.accountPassword.getEditableText().toString(); + if (password.isEmpty()) { + enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password)); + return; + } + final Uri uri = backupFile.getUri(); + Intent intent = new Intent(this, ImportBackupService.class); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra("password", password); + if ("file".equals(uri.getScheme())) { + intent.putExtra("file", uri.getPath()); + } else { + intent.setData(uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + setLoadingState(true); + ContextCompat.startForegroundService(this, intent); + d.dismiss(); + }); + }); + dialog.show(); + } + + private void setLoadingState(final boolean loadingState) { + binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); + binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); + setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); + configureActionBar(getSupportActionBar(), !loadingState); + this.mLoadingState = loadingState; + invalidateOptionsMenu(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + if (resultCode == RESULT_OK) { + if (requestCode == 0xbac) { + openBackupFileFromUri(intent.getData(), false); + } + } + } + + @Override + public void onAccountAlreadySetup() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public void onBackupRestored() { + runOnUiThread(() -> { + Intent intent = new Intent(this, ConversationActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }); + } + + @Override + public void onBackupDecryptionFailed() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public void onBackupRestoreFailed() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_open_backup_file) { + openBackupFile(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void openBackupFile() { + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..3419d8fc95638f975ecf630e46cc442ca92ee652 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -0,0 +1,164 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Toast; + +import androidx.databinding.DataBindingUtil; + +import java.security.SecureRandom; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.MagicCreateBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.InstallReferrerUtils; +import eu.siacs.conversations.xmpp.Jid; + +public class MagicCreateActivity extends XmppActivity implements TextWatcher { + + public static final String EXTRA_DOMAIN = "domain"; + public static final String EXTRA_PRE_AUTH = "pre_auth"; + public static final String EXTRA_USERNAME = "username"; + + private MagicCreateBinding binding; + private String domain; + private String username; + private String preAuth; + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + final Intent data = getIntent(); + this.domain = data == null ? null : data.getStringExtra(EXTRA_DOMAIN); + this.preAuth = data == null ? null : data.getStringExtra(EXTRA_PRE_AUTH); + this.username = data == null ? null : data.getStringExtra(EXTRA_USERNAME); + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create); + setSupportActionBar(this.binding.toolbar); + configureActionBar(getSupportActionBar(), this.domain == null); + if (username != null && domain != null) { + binding.title.setText(R.string.your_server_invitation); + binding.instructions.setText(getString(R.string.magic_create_text_fixed, domain)); + binding.finePrint.setVisibility(View.INVISIBLE); + binding.username.setEnabled(false); + binding.username.setText(this.username); + updateFullJidInformation(this.username); + } else if (domain != null) { + binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain)); + binding.finePrint.setVisibility(View.INVISIBLE); + } + binding.createAccount.setOnClickListener(v -> { + try { + final String username = binding.username.getText().toString(); + final Jid jid; + final boolean fixedUsername; + if (this.domain != null && this.username != null) { + fixedUsername = true; + jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain); + } else if (this.domain != null) { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, this.domain); + } else { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); + } + if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) { + binding.username.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); + } else { + binding.username.setError(null); + Account account = xmppConnectionService.findAccountByJid(jid); + if (account == null) { + account = new Account(jid, CryptoHelper.createPassword(new SecureRandom())); + account.setOption(Account.OPTION_REGISTER, true); + account.setOption(Account.OPTION_DISABLED, true); + account.setOption(Account.OPTION_MAGIC_CREATE, true); + account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); + if (this.preAuth != null) { + account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + } + xmppConnectionService.createAccount(account); + } + Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("init", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show(); + StartConversationActivity.addInviteUri(intent, getIntent()); + startActivity(intent); + } + } catch (IllegalArgumentException e) { + binding.username.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); + } + }); + binding.username.addTextChangedListener(this); + } + + @Override + public void onDestroy() { + InstallReferrerUtils.markInstallReferrerExecuted(this); + super.onDestroy(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(final Editable s) { + updateFullJidInformation(s.toString()); + } + + private void updateFullJidInformation(final String username) { + if (username.trim().isEmpty()) { + binding.fullJid.setVisibility(View.INVISIBLE); + } else { + try { + binding.fullJid.setVisibility(View.VISIBLE); + final Jid jid; + if (this.domain == null) { + jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); + } else { + jid = Jid.ofLocalAndDomainEscaped(username, this.domain); + } + binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString())); + } catch (IllegalArgumentException e) { + binding.fullJid.setVisibility(View.INVISIBLE); + } + } + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..c1ee451be7ac01ac8a25b3e7fd7cddedf6885750 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,428 @@ +package eu.siacs.conversations.ui; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.util.Pair; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; + +import org.openintents.openpgp.util.OpenPgpApi; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; + +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + +public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { + + private final String STATE_SELECTED_ACCOUNT = "selected_account"; + + private static final int REQUEST_IMPORT_BACKUP = 0x63fb; + + protected Account selectedAccount = null; + protected Jid selectedAccountJid = null; + + protected final List accountList = new ArrayList<>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false); + + protected Pair mPostponedActivityResult = null; + + @Override + public void onAccountUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + synchronized (this.accountList) { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setHomeButtonEnabled(this.accountList.size() > 0); + actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0); + } + invalidateOptionsMenu(); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_manage_accounts); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + if (savedInstanceState != null) { + String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT); + if (jid != null) { + try { + this.selectedAccountJid = Jid.ofEscaped(jid); + } catch (IllegalArgumentException e) { + this.selectedAccountJid = null; + } + } + } + + accountListView = findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); + registerForContextMenu(accountListView); + } + + @Override + protected void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + public void onSaveInstanceState(final Bundle savedInstanceState) { + if (selectedAccount != null) { + savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); + } + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); + AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedAccount = accountList.get(acmi.position); + if (this.selectedAccount.isEnabled()) { + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp()); + } else { + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); + menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); + } + menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toEscapedString()); + } + + @Override + void onBackendConnected() { + if (selectedAccountJid != null) { + this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid); + } + refreshUiReal(); + if (this.mPostponedActivityResult != null) { + this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); + } + if (Config.X509_VERIFICATION && this.accountList.size() == 0) { + if (mInvokedAddAccount.compareAndSet(false, true)) { + addAccountFromKey(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + MenuItem enableAll = menu.findItem(R.id.action_enable_all); + MenuItem addAccount = menu.findItem(R.id.action_add_account); + MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert); + + if (Config.X509_VERIFICATION) { + addAccount.setVisible(false); + addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + + if (!accountsLeftToEnable()) { + enableAll.setVisible(false); + } + MenuItem disableAll = menu.findItem(R.id.action_disable_all); + if (!accountsLeftToDisable()) { + disableAll.setVisible(false); + } + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mgmt_account_publish_avatar: + publishAvatar(selectedAccount); + return true; + case R.id.mgmt_account_disable: + disableAccount(selectedAccount); + return true; + case R.id.mgmt_account_enable: + enableAccount(selectedAccount); + return true; + case R.id.mgmt_account_delete: + deleteAccount(selectedAccount); + return true; + case R.id.mgmt_account_announce_pgp: + publishOpenPGPPublicKey(selectedAccount); + return true; + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (MenuDoubleTabUtil.shouldIgnoreTap()) { + return false; + } + switch (item.getItemId()) { + case R.id.action_add_account: + startActivity(new Intent(this, EditAccountActivity.class)); + break; + case R.id.action_import_backup: + if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { + startActivity(new Intent(this, ImportBackupActivity.class)); + } + break; + case R.id.action_disable_all: + disableAllAccounts(); + break; + case R.id.action_enable_all: + enableAllAccounts(); + break; + case R.id.action_add_account_with_cert: + addAccountFromKey(); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (grantResults.length > 0) { + if (allGranted(grantResults)) { + switch (requestCode) { + case REQUEST_IMPORT_BACKUP: + startActivity(new Intent(this, ImportBackupActivity.class)); + break; + } + } else { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } + } + if (writeGranted(grantResults, permissions)) { + if (xmppConnectionService != null) { + xmppConnectionService.restartFileObserver(); + } + } + } + + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish + // animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + + @Override + public void onClickTglAccountState(Account account, boolean enable) { + if (enable) { + enableAccount(account); + } else { + disableAccount(account); + } + } + + private void addAccountFromKey() { + try { + KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } + } + + private void publishAvatar(Account account) { + Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } + + private void disableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isEnabled()) { + list.add(account); + } + } + } + for (Account account : list) { + disableAccount(account); + } + } + + private boolean accountsLeftToDisable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isEnabled()) { + return true; + } + } + return false; + } + } + + private boolean accountsLeftToEnable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isEnabled()) { + return true; + } + } + return false; + } + } + + private void enableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isEnabled()) { + list.add(account); + } + } + } + for (Account account : list) { + enableAccount(account); + } + } + + private void disableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, true); + if (!xmppConnectionService.updateAccount(account)) { + Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); + } + } + + private void enableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, false); + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetEverything(); + } + if (!xmppConnectionService.updateAccount(account)) { + Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); + } + } + + private void publishOpenPGPPublicKey(Account account) { + if (ManageAccountActivity.this.hasPgp()) { + announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); + } else { + this.showInstallPgpDialog(); + } + } + + private void deleteAccount(final Account account) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + (dialog, which) -> { + xmppConnectionService.deleteAccount(account); + selectedAccount = null; + if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + WelcomeActivity.launch(this); + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (xmppConnectionServiceBound) { + if (requestCode == REQUEST_CHOOSE_PGP_ID) { + if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { + selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID)); + announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); + } else { + choosePgpSignId(selectedAccount); + } + } else if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished); + } + this.mPostponedActivityResult = null; + } else { + this.mPostponedActivityResult = new Pair<>(requestCode, data); + } + } + } + + @Override + public void alias(final String alias) { + if (alias != null) { + xmppConnectionService.createAccountFromKey(alias, this); + } + } + + @Override + public void onAccountCreated(final Account account) { + final Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("init", true); + startActivity(intent); + } + + @Override + public void informUser(final int r) { + runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show()); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..06320d33deea510e9edd2b139757ce2ec8c08373 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityPickServerBinding; +import eu.siacs.conversations.entities.Account; + +public class PickServerActivity extends XmppActivity { + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == android.R.id.home) { + startActivity(new Intent(this, WelcomeActivity.class)); + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + startActivity(new Intent(this, WelcomeActivity.class)); + super.onBackPressed(); + } + + @Override + public void onNewIntent(Intent intent) { + if (intent != null) { + setIntent(intent); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + binding.useCim.setOnClickListener(v -> { + final Intent intent = new Intent(this, MagicCreateActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + addInviteUri(intent); + startActivity(intent); + }); + binding.useOwnProvider.setOnClickListener(v -> { + List accounts = xmppConnectionService.getAccounts(); + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, true); + if (accounts.size() == 1) { + intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); + intent.putExtra("init", true); + } else if (accounts.size() >= 1) { + intent = new Intent(this, ManageAccountActivity.class); + } + addInviteUri(intent); + startActivity(intent); + }); + + } + + public void addInviteUri(Intent intent) { + StartConversationActivity.addInviteUri(intent, getIntent()); + } + + public static void launch(AppCompatActivity activity) { + Intent intent = new Intent(activity, PickServerActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(0, 0); + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..762dfbb422780c20293eec13c3acdcd846d2595e --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java @@ -0,0 +1,90 @@ +package eu.siacs.conversations.ui; + +import android.os.Bundle; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import eu.siacs.conversations.xmpp.Jid; + +public class ShareViaAccountActivity extends XmppActivity { + public static final String EXTRA_CONTACT = "contact"; + public static final String EXTRA_BODY = "body"; + + protected final List accountList = new ArrayList<>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + + @Override + protected void refreshUiReal() { + synchronized (this.accountList) { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + } + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_manage_accounts); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + accountListView = findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList, false); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener((arg0, view, position, arg3) -> { + final Account account = accountList.get(position); + final String body = getIntent().getStringExtra(EXTRA_BODY); + + try { + final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); + final Conversation conversation = xmppConnectionService.findOrCreateConversation( + account, contact, false, false); + switchToConversation(conversation, body); + } catch (IllegalArgumentException e) { + // ignore error + } + + finish(); + }); + } + + @Override + protected void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + void onBackendConnected() { + final int numAccounts = xmppConnectionService.getAccounts().size(); + + if (numAccounts == 1) { + final String body = getIntent().getStringExtra(EXTRA_BODY); + final Account account = xmppConnectionService.getAccounts().get(0); + + try { + final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); + final Conversation conversation = xmppConnectionService.findOrCreateConversation( + account, contact, false, false); + switchToConversation(conversation, body); + } catch (IllegalArgumentException e) { + // ignore error + } + + finish(); + } else { + refreshUiReal(); + } + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..8f652ce8e522bef41e4631c504aaacf3c22647cf --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -0,0 +1,234 @@ +package eu.siacs.conversations.ui; + +import android.Manifest; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.net.Uri; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import java.util.Arrays; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityWelcomeBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Compatibility; +import eu.siacs.conversations.utils.InstallReferrerUtils; +import eu.siacs.conversations.utils.SignupUtils; +import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xmpp.Jid; + +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + +public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { + + private static final int REQUEST_IMPORT_BACKUP = 0x63fb; + + private XmppUri inviteUri; + + public static void launch(AppCompatActivity activity) { + Intent intent = new Intent(activity, WelcomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(0, 0); + } + + public void onInstallReferrerDiscovered(final Uri referrer) { + Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer); + if ("xmpp".equalsIgnoreCase(referrer.getScheme())) { + final XmppUri xmppUri = new XmppUri(referrer); + runOnUiThread(() -> processXmppUri(xmppUri)); + } else { + Log.i(Config.LOGTAG, "install referrer was not an XMPP uri"); + } + } + + private void processXmppUri(final XmppUri xmppUri) { + if (!xmppUri.isValidJid()) { + return; + } + final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); + final Jid jid = xmppUri.getJid(); + final Intent intent; + if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { + intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); + } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { + intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); + intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); + } else { + intent = null; + } + if (intent != null) { + startActivity(intent); + finish(); + return; + } + this.inviteUri = xmppUri; + } + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + new InstallReferrerUtils(this); + } + + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void onNewIntent(Intent intent) { + if (intent != null) { + setIntent(intent); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar(), false); + binding.registerNewAccount.setOnClickListener(v -> { + final Intent intent = new Intent(this, PickServerActivity.class); + addInviteUri(intent); + startActivity(intent); + }); + binding.useExisting.setOnClickListener(v -> { + final List accounts = xmppConnectionService.getAccounts(); + Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class); + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false); + if (accounts.size() == 1) { + intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); + intent.putExtra("init", true); + } else if (accounts.size() >= 1) { + intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class); + } + addInviteUri(intent); + startActivity(intent); + }); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.welcome_menu, menu); + final MenuItem scan = menu.findItem(R.id.action_scan_qr_code); + scan.setVisible(Compatibility.hasFeatureCamera(this)); + return super.onCreateOptionsMenu(menu); + } + + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_import_backup: + if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { + startActivity(new Intent(this, ImportBackupActivity.class)); + } + break; + case R.id.action_scan_qr_code: + UriHandlerActivity.scan(this, true); + break; + case R.id.action_add_account_with_cert: + addAccountFromKey(); + break; + } + return super.onOptionsItemSelected(item); + } + + private void addAccountFromKey() { + try { + KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } + } + + @Override + public void alias(final String alias) { + if (alias != null) { + xmppConnectionService.createAccountFromKey(alias, this); + } + } + + @Override + public void onAccountCreated(final Account account) { + final Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); + intent.putExtra("init", true); + addInviteUri(intent); + startActivity(intent); + } + + @Override + public void informUser(final int r) { + runOnUiThread(() -> Toast.makeText(this, r, Toast.LENGTH_LONG).show()); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); + if (grantResults.length > 0) { + if (allGranted(grantResults)) { + switch (requestCode) { + case REQUEST_IMPORT_BACKUP: + startActivity(new Intent(this, ImportBackupActivity.class)); + break; + } + } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } + } + if (writeGranted(grantResults, permissions)) { + if (xmppConnectionService != null) { + xmppConnectionService.restartFileObserver(); + } + } + } + + public void addInviteUri(Intent to) { + final Intent from = getIntent(); + if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) { + final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI); + to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite); + } else if (this.inviteUri != null) { + Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow"); + to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString()); + } + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..9857dcd8ac34e664c38f8f68a99366d9f139bfe5 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java @@ -0,0 +1,170 @@ +package eu.siacs.conversations.ui.adapter; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.text.format.DateUtils; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.AccountRowBinding; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.Jid; + +public class BackupFileAdapter extends RecyclerView.Adapter { + + private OnItemClickedListener listener; + + private final List files = new ArrayList<>(); + + + @NonNull + @Override + public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false)); + } + + @Override + public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) { + final ImportBackupService.BackupFile backupFile = files.get(position); + final BackupFileHeader header = backupFile.getHeader(); + backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString()); + backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s",header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR))); + backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE); + backupFileViewHolder.binding.getRoot().setOnClickListener(v -> { + if (listener != null) { + listener.onClick(backupFile); + } + }); + loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage); + } + + @Override + public int getItemCount() { + return files.size(); + } + + public void setFiles(List files) { + this.files.clear(); + this.files.addAll(files); + notifyDataSetChanged(); + } + + public void setOnItemClickedListener(OnItemClickedListener listener) { + this.listener = listener; + } + + static class BackupFileViewHolder extends RecyclerView.ViewHolder { + private final AccountRowBinding binding; + + BackupFileViewHolder(AccountRowBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + } + + public interface OnItemClickedListener { + void onClick(ImportBackupService.BackupFile backupFile); + } + + static class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Jid jid = null; + private final int size; + + BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics(); + this.size = ((int) (48 * metrics.density)); + } + + @Override + protected Bitmap doInBackground(Jid... params) { + this.jid = params[0]; + return AvatarService.get(this.jid, size); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && !isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + private void loadAvatar(Jid jid, ImageView imageView) { + if (cancelPotentialWork(jid, imageView)) { + imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(jid); + } catch (final RejectedExecutionException ignored) { + } + } + } + + private static boolean cancelPotentialWork(Jid jid, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Jid oldJid = bitmapWorkerTask.jid; + if (oldJid == null || jid != oldJid) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..593291d95374f4be2f1722b8c46a03fe34ed28dc --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java @@ -0,0 +1,43 @@ +package eu.siacs.conversations.utils; + +import android.app.Activity; +import android.content.Intent; +import android.widget.Toast; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.AccountConfiguration; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditAccountActivity; +import eu.siacs.conversations.xmpp.Jid; + +public class ProvisioningUtils { + + public static void provision(final Activity activity, final String json) { + final AccountConfiguration accountConfiguration; + try { + accountConfiguration = AccountConfiguration.parse(json); + } catch (final IllegalArgumentException e) { + Toast.makeText(activity, R.string.improperly_formatted_provisioning, Toast.LENGTH_LONG).show(); + return; + } + final Jid jid = accountConfiguration.getJid(); + final List accounts = DatabaseBackend.getInstance(activity).getAccountJids(true); + if (accounts.contains(jid)) { + Toast.makeText(activity, R.string.account_already_exists, Toast.LENGTH_LONG).show(); + return; + } + final Intent serviceIntent = new Intent(activity, XmppConnectionService.class); + serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT); + serviceIntent.putExtra("address", jid.asBareJid().toEscapedString()); + serviceIntent.putExtra("password", accountConfiguration.password); + Compatibility.startService(activity, serviceIntent); + final Intent intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", jid.asBareJid().toEscapedString()); + intent.putExtra("init", true); + activity.startActivity(intent); + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..fb088234a24e10ea315bafebd910e28df9caa270 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java @@ -0,0 +1,77 @@ +package eu.siacs.conversations.utils; + +import android.app.Activity; +import android.content.Intent; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.ui.EditAccountActivity; +import eu.siacs.conversations.ui.MagicCreateActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.ui.PickServerActivity; +import eu.siacs.conversations.ui.StartConversationActivity; +import eu.siacs.conversations.ui.WelcomeActivity; +import eu.siacs.conversations.xmpp.Jid; + +public class SignupUtils { + + public static boolean isSupportTokenRegistry() { + return true; + } + + public static Intent getTokenRegistrationIntent(final Activity activity, Jid jid, String preAuth) { + final Intent intent = new Intent(activity, MagicCreateActivity.class); + if (jid.isDomainJid()) { + intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString()); + } else { + intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString()); + intent.putExtra(MagicCreateActivity.EXTRA_USERNAME, jid.getEscapedLocal()); + } + intent.putExtra(MagicCreateActivity.EXTRA_PRE_AUTH, preAuth); + return intent; + } + + public static Intent getSignUpIntent(final Activity activity) { + return getSignUpIntent(activity, false); + } + + public static Intent getSignUpIntent(final Activity activity, final boolean toServerChooser) { + final Intent intent; + if (toServerChooser) { + intent = new Intent(activity, PickServerActivity.class); + } else { + intent = new Intent(activity, WelcomeActivity.class); + } + return intent; + } + + public static Intent getRedirectionIntent(final ConversationsActivity activity) { + final XmppConnectionService service = activity.xmppConnectionService; + Account pendingAccount = AccountUtils.getPendingAccount(service); + Intent intent; + if (pendingAccount != null) { + intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", pendingAccount.getJid().asBareJid().toString()); + if (!pendingAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, pendingAccount.isOptionSet(Account.OPTION_REGISTER)); + } + } else { + if (service.getAccounts().size() == 0) { + if (Config.X509_VERIFICATION) { + intent = new Intent(activity, ManageAccountActivity.class); + } else if (Config.MAGIC_CREATE_DOMAIN != null) { + intent = getSignUpIntent(activity); + } else { + intent = new Intent(activity, EditAccountActivity.class); + } + } else { + intent = new Intent(activity, StartConversationActivity.class); + } + } + intent.putExtra("init", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } +} \ No newline at end of file diff --git a/src/cheogram/new_launcher-web.png b/src/cheogram/new_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..76057f9fea7b2eded734a58069a67c575d466167 GIT binary patch literal 19483 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 literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..18730f12f8b7a94fc49b26a916aa877091d3a9c2 GIT binary patch literal 258 zcmV+d0sa1oP)Icq!0;eqabRbNulwHn5f6%?fAxJW0t(R)$X0yb7#KU znF(4--42WZYrw~d_}TW`|3QzbH`I6jtKaG~bz0rG8Snx1q3!fTomF?Vg`ZZxw1nqc zRL3`hPgWz^tUjot>%sS^pZkC(&8u6Q0_F<9lg=e>q(uc@r1t`P3+#;u2P$K#ARJ&f za1#{NF3pK6PAtMbP)?`h=?s%w?E zoH1V37gNP64Mf?}&O9*q?njq+YCMT;F8h^BLSL_=_e@LvIZpG}N$P zEkJJ{i~?_ft%cxUft?Z2832fg_rUc+aDdwp(Scrx04~$rfh9E3=&Fa*l>!UAq&A-n z>S6VDfsljs5&;kq&*`2cGr(#eWt!ZuhdQQSP#@X7msizq>MQlWdS0DS&!|uA-qlHW s`xxB11Da5;s>|wP@qjF7n>q~e2Zt*yOg8&mI{*Lx07*qoM6N<$g2U$Rc>n+a literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8ec62cd34b2c63234ff5593637d5105bdb253343 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i8c!F;5R22v2@-oA?Em)v`2WI2 z_y46Ar*{9@|9wC2@ECHuegCV^l*HW)V~E;3QPf!hnW>r91l1D=W_Vb z5W(y4G{MkBcZZ3?)a0y#>li&PcZualU6`^s>)_+eKZ~whaF{%!&NbjKcL*=z|M*AY f|L&Jxcvj00UZF6vf&ID(&>ak(u6{1-oD!MiE6^m#!y78w- zEn-orxah7>1*s^4g5pXLL_`!pyAX6~#h?XI1gR*sp!l;;NNuS_g{sYu#+W=VPHvog za?j1V|LL>2cRs#z&il@snK?59XE@RB8>;Jo%YZ>(7?=g7f#bkLM9j7Gc`X}IHvo47 zw*c1z=haD{1HJ)10mgxkBVxW)4Gp7)x=sC1ows>TsL!bDyD5IFdZY!>wU}Nsa!x0O z52&xH3vCvkGNInk3GtiM?>i_wWlnv(o#MBszjaA`$}4JrtKzRxfA4|#3j5XOMoRT! z_17MXukds;;?GhK^;-EN3+nbFt$hXYCxPo4cHS8RcvW3fD%)(t`MbB3UXz!L+~p0t z1>DkHe;C$fCI`@1QSRWA^fCm6e=vcQb zVz!uAiQ975>?p{4F_$-}{!=6GsJX9A_5Bh$?yKadEl_50eQJerQGAAMcOuUp5s|O) zqI;$+X-y53c~|#TYE10|_7vppQiqfBgJjz*d9GA%tQ6EyU|R_tQf6o2{70&fF<0Xu=UB{Xb}h)>P(;k6!DW+6L`(vEyQQiD z`hd-~g3E}A1Hen2R@4MHwlTfG(_ zB2JU7l@E4SS7`=-;$gHF5itd9C+icd;ZtKQEv#q&AR=ZXVlSDI;~fyy2vz-{;ll%U zfNYEJA$l`oPXTLNK5tfs)Q8o>CDyRDSWv%E_oP~B%4Xb_X?dm&bhkZpn)>Z>`jGf)WwIk|N>aiLI-fgGOj-vLf tcdG9tt@=i0(W05oo#=O){{WDLbw%6UvylJ*002ovPDHLkV1mXz@hJcR literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a1509a16df382eb674cddbaf52e6e70292b30c GIT binary patch literal 273 zcmV+s0q*{ZP)+fBM5T$P+WqDqf*6fiTYQEJx&}*G=J0NWgzCp99W#r?rXGJn%$B z>pbv5M4LPi)c^)CmvK+P2;p9U(bwDqF#0w}14hXl2^jq{M*&8eIRY@6nOgy)9dip{ zR4~^9Mul?~kR&SJ4&V>og8^)aXvzZ*M6}8SC%n+wGm8XBh&2N{E z&fGgQ_hn{h{lWcmo;l|{^XJUWIp=xKg_E458Z51jC5-_l0TY3dz?r}>-~`YKbOZZ< z-DdWCN^UkrAW5eHGl1EJ-wdERMiWPXAA$9S-$pb0r267fQWnLhSJF~RL(>#ygrw!^>QIr5lEzkZpNT&6lA3|ffUBzMyg}>-=9<~I zB$Z^qgIF{klbwbD_~Ya$Fjse;8HXDE6T+~A~yOW`49(SEU-cmCzKThl5_*` zRJC4Zj5)y5Q7$a|h)Eg_v;*hWz^SZp47kM1c9y%S(g=Eie2N2bD)6?XvV$blC}|?F z3rKIe-WYB&v(;h7awF(j@-Gg+l5(Rc&>(3Xum`sVIvAK|W?uvu!bZ>o3?6X+9tks) zj37xvfW5%@dU}*GdVvXMw$C>Nji5FLmpI^bxFgKqGrzvRWR`hBg+Y&4J8*H`-N_z3 zz-Tk;DlY6df+U>V<6OBIc;1(pMU0yBZff#DJB9sphgwgTgThk@xa`d$UB#KrL2fX7I-Oe!A%k+d#h zc;zjBJtJwVTnxPue@HsltJ@@Lb;Q1(54evuNj(Yf<47feHYaGbOK#sNsJ&Ms)L$4< zcU*+Jdlj-XDCzwK_ab@85dJ$T$@;6^%(_Etn)f|bFVHXV$QP)K__Ruz;};f&>b()_-yc$bCr~M+<#I_= z1Nt5T+!mp4pKdc#s8!O6gwJh+Y6xMN!=rNo!*C zUCe%86aUNe67XCC<@@NsedZ?<|1*0O*n!(g7?Ysy-+;roxlGa=p`B)SQE`Dk5Beox z!+o@nd@gkw0aN`LOsJx6AG`em{~h#m4LTXflnfAGpsgf(QRjv70-0G45G(s3XKeQi zf_4!rxY&DjbR>%$Bc?#B zsD4f~kXUJE2Z9WN0u@%sFW1$j>>%KcFk`vwjwW1gP~P{cKQXffVaCwxj=~%LL7c*Q zret|hOxXk~GuwdMXU;Qab1y5#z~r*(x8P0&=8+zt*~|_F87n4nn%O~M0p3nqj%dO~ zVFHPi23$Dju&7oZWsuonhNyYcnAtl(Nr)vUTpMOca+u|HTu?AioWQ+@4*Q14Bd8BE zdjeQYzCQm$2|3yoTOa^4TMFC<9IwKutP%LRPb3h4nJvSqz9(rL86!}js&j!PodLWF z+*OPAQsg)=%8wE!9zlJWS(lmJgZoteRBd}HG7Km=EnT%heH4}@({Yi~vg?}+Gutny znLywcxMub)ZU^92;A>pGyI%AH-{QWi*8$6LM=z7o_%m^m#!Gre?uhpBgdyQ#4oX@j zX`up+d`mi0(xZ~v6SRF)-d|ia^rb47RB$zL87|##67GQTNcv}^9mR!_4*X_V^lZwYeA6pJP8h%kzl)Vluxg``=M zJ}5j+L426DkC{Ny7)eh`+T{t<@65(Pp4im{x#ZI}+(F+>X7+i?E}bM-{0EOv#DnMD RppXCn002ovPDHLkV1mg6@QDBb literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..20d015751f5e7a7654fecd4d1beb6ccdb1033f8b GIT binary patch literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKN!06}c;uuoF`1ZV^7jvON>&JQp zm*xkIOkLACniS#=UAm>$D(CP);GQF^cC>4buxhJZg0$ntc##tO*11-DT24+#+IO9Q zzh?2&)W~TY&&!&|UA_h3|*nY@Hy z!WqLL29IYFnhYw%JtC||eNJ5rlJ2TI8%z59KCw$4?_AOhW%Vv;zGLZD#LD^KGR7mI zN$5#PlhBj}`ioYXUN8>qHNBu2*lTjZYQ2le(&dd@(R)}5*hEA_A|AJJnx0;fcBVtl imEBd(5#+G2och=Q?sSz#RHy+1n8DN4&t;ucLK6T>dYn`M literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_notification.png b/src/cheogram/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..65d106b8c3bb408e7385900b93ff5481e9ce6e93 GIT binary patch literal 2117 zcmV-L2)g%)P)f+6i4SUsiJAz(#Q1=q36U5RwV|dO@dhR!y@j+QMygoDr6Ay?L?XprC~fHF z^I`r5X!qV{-_AKd|JmQ;nK^6LS?kQ6nLV@CKu1SM$IysjE6@w|SYR$N4>%r}3A_?G z0+7FYxPF(RI8 zrsX6L=Ar!?klyHmV;Swyj!4ou}S0WW!cvrF!bHZPy5OntGkOXSlYk z&2IHt^=19EQ(vZ`>M6hq;7$FIKVbd@EQ*Ln`XRjaK}TIoQhG8NAK)xtgZk-y2ye{+ z>Qwbc;K!s@>SSUjaI^XyTb?Pk@(5C24lDx}wjpnVb2G4{trc}+2dGB^w*u!jSLY?e zoxn#TVrNr9jT~TF&fgBav#BC4K~@7Fh=|=ygw%eXjZe>h&S)AR;N8Fq^{^&FYB^wh zAX*Mw)THrAz=wdFnh2=d1HJ-$yh+28h$ZTkb-!GB3{b~_mB7@xjZQ-L0AmsH`>J28 z3gPiOo*UIScT^-HCDfcjC=tf!-x1rf2bY~@l8 zP-g=71Fx>D)d=wjFu&~jpzI^)YC7YWb^c@DUsPs)rc@5w9Z955Ji?Rm%;FuvWmoym8$XP7)05yxc}Wc7g8 z(iy*PW~-TzK3NVprO(QYDwzXu=YYYHY1gbXEpz(N_~WDlyHkL-11C0=w->k@*bK}D z&H)Z@D02(29@qt(4zwK~Q0}dQ`lA-l*l$-CjlWkqAb*N_x%C6`)~It1()k4SH#Oz$ zRljOF-}*wC3)ClS%FN=sWgT!!OAYQb9bw69=fgE*-eU@7&5}3O6lqzNFBIr_s`^X~ znOXd>tOM%&h%A0qqOFw`WfmTTR8OlYZp|$SYz` z)`wXR*wIphZzxeG_a#*Z$C-}DhPukhjyMPhNV z>edD_|E4ZfU!_i0&sJA7ke9_bvh1Dp@B9QQ#=i+36k~|g>+p`Mt9I&d7%8VlSaZP{6A^_wYCWSQBVsKD^Vp7DZs=uvC?6?*CU6g3hLp1G299qlPMC90I-|*XWh#|+Kt${SmXcnrcJyYfOr=Wsf+FHp;JP~6j3A|YK%JCj zhmrORMm1YvStJd_PZm|GoU|_@_5zDYFPS>>2~x0oA+@q5M#N^|y|j01Z?e+!F;qKo zYeZ}W-cQ*BN2AGDuL^DCj=m816>09<@nQsKN5p^gDl|^T9uaqt?)<8hu5m;NdOU_& z$%P*ge;9A*pKZvSWE2j>tzAYC5f1?GpxaA6n6aY5c5j17v!nBXYv@i@9N>-WF?ofp z-ystbdn4j%;6l>HnxjrouxRbu&2bU2iWGrz4efw(7aSmw}51+3o~jKX7c;4%t92uZ)Ow5pfCdHqvp8%*eq3vyXIgZ7J|s z`m_Byg^75{k$Q}JrMh0--*QM$5zncs)urkod3;O}@;9hYx7cRpYy1gwqqZsk!vfN( zdJM=+a8#51q!Y@kNUzfGDsu&uDUx#`a5-s>(}WEXaY%sSgmJ*Zm~Jzj&-3@m&jn5( zMK&IsP;fsfl72fW%JWfDsMQ0cwEGYBN+DcApZUNQq|n+~HAL(Kj&7>Qgh8F59;FU6 zyO$~2bBXmXr5?4(M}JYzP;WA&y vT99@O0(F*Yiv4SKxth5W?&#>~7$W}%LUnph+k0?_00000NkvXXu0mjf!&w5E literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a789520baa7f4b65fd73ae78a57ec50b9d645e3c GIT binary patch literal 503 zcmV9JZFUK03OixFK?s74p2SYCAYR}p1?>_^(85!gQmq9eh^_{-&k=SI_HVTg zh?@Ooznyspp8wN-!>=89()iz$2!_W(zjkExyoapP1JK?6aWn z7F@3dm~h4roiV8cct+cOVyBz&mbRxg01s(5V4Wa(uWz#a0m;&wUU1(`Z22Ye?}7v+GT zWU46#Bx{5K0}L?000Rs#zyP5!14IyLtN{IfOJf9xXotoI5b?(}CV+^)Ok)9v_{TH` zfQbK0*#RQ1m@)%IY%pa7h}dw-2oO;)Wdn#PoH7AKbeOULM0A`o07Pst4Gj>n7+Ap}a$4Sn!<>CMU002ovPDHLkV1mEb(=Px3 literal 0 HcmV?d00001 diff --git a/src/cheogram/res/drawable/background.xml b/src/cheogram/res/drawable/background.xml new file mode 100644 index 0000000000000000000000000000000000000000..32bc72f1c2329d73914f9930fddc38c244397ef2 --- /dev/null +++ b/src/cheogram/res/drawable/background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/cheogram/res/drawable/bar_logo.xml b/src/cheogram/res/drawable/bar_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ce2ae9f531625c3065d1cacbd2eb378c9a612d7 --- /dev/null +++ b/src/cheogram/res/drawable/bar_logo.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/cheogram/res/drawable/ic_launcher.xml b/src/cheogram/res/drawable/ic_launcher.xml new file mode 100644 index 0000000000000000000000000000000000000000..4be18371e4589579fe65ad4763e9f399862607a2 --- /dev/null +++ b/src/cheogram/res/drawable/ic_launcher.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/res/drawable/main_logo.xml b/src/cheogram/res/drawable/main_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fb672cd3c4426fa4e6ef1209eefbd584bbd3e81 --- /dev/null +++ b/src/cheogram/res/drawable/main_logo.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/res/drawable/splash_logo.xml b/src/cheogram/res/drawable/splash_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..500d002e446e54153c4bc122f36152bdcd2ca79a --- /dev/null +++ b/src/cheogram/res/drawable/splash_logo.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/cheogram/res/layout/activity_easy_invite.xml b/src/cheogram/res/layout/activity_easy_invite.xml new file mode 100644 index 0000000000000000000000000000000000000000..8bbf11c037e00c299f152d5084093335d7f5f607 --- /dev/null +++ b/src/cheogram/res/layout/activity_easy_invite.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + +