diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml index 1697e401a91b5584bdfb96bcc62406e11a32dbe7..299f65672c4201fda1f83b0e40aadf1b8bd67f4a 100644 --- a/.builds/debian-stable.yml +++ b/.builds/debian-stable.yml @@ -22,6 +22,6 @@ tasks: yes | android/cmdline-tools/tools/bin/sdkmanager --licenses - build: | cd cheogram-android - ./gradlew assembleConversationsFreeCompatDebug + ./gradlew assembleCheogramFreeSystemDebug - assets: | - mv cheogram-android/build/outputs/apk/conversationsFreeCompat/debug/*.apk cheogram.apk + mv cheogram-android/build/outputs/apk/cheogramFreeSystem/debug/*.apk cheogram.apk diff --git a/build.gradle b/build.gradle index 9adcfd56692cb4df268bfbebfbb92295bbdb1d3e..78285540ecd45732a3d8a91fef2ac8ca1630c2b6 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,17 @@ android { dimension "mode" } + cheogram { + dimension "mode" + + applicationId = "com.cheogram.android" + resValue "string", "applicationId", applicationId + + def appName = "Cheogram" + resValue "string", "app_name", appName + buildConfigField "String", "APP_NAME", "\"$appName\""; + } + playstore { dimension "distribution" versionNameSuffix "+p" @@ -200,6 +211,17 @@ android { srcDir 'src/conversationsFree/java' } } + cheogramFreeCompat { + java { + srcDir 'src/freeCompat/java' + srcDir 'src/conversationsFree/java' + } + } + cheogramFreeSystem { + java { + srcDir 'src/conversationsFree/java' + } + } conversationsPlaystoreCompat { java { srcDir 'src/playstoreCompat/java' diff --git a/src/cheogram/AndroidManifest.xml b/src/cheogram/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..62396bed1af27fcdcac16db152115fbd362223ec --- /dev/null +++ b/src/cheogram/AndroidManifest.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..65b6804f9b6e7bb4abac560824bf5958a38dbd0b --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/entities/AccountConfiguration.java @@ -0,0 +1,50 @@ +package eu.siacs.conversations.entities; + +import com.google.common.base.Preconditions; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +import eu.siacs.conversations.xmpp.Jid; + +public class AccountConfiguration { + + private static final Gson GSON = new GsonBuilder().create(); + + public Protocol protocol; + public String address; + public String password; + + public Jid getJid() { + return Jid.ofEscaped(address); + } + + public static AccountConfiguration parse(final String input) { + final AccountConfiguration c; + try { + c = GSON.fromJson(input, AccountConfiguration.class); + } catch (JsonSyntaxException e) { + throw new IllegalArgumentException("Not a valid JSON string", e); + } + Preconditions.checkArgument( + c.protocol == Protocol.XMPP, + "Protocol must be XMPP" + ); + Preconditions.checkArgument( + c.address != null && c.getJid().isBareJid() && !c.getJid().isDomainJid(), + "Invalid XMPP address" + ); + Preconditions.checkArgument( + c.password != null && c.password.length() > 0, + "No password specified" + ); + return c; + } + + public enum Protocol { + @SerializedName("xmpp") XMPP, + } + +} + diff --git a/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java new file mode 100644 index 0000000000000000000000000000000000000000..9c6ebaafd08e438e48f371d3a0a3c6f61714138e --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java @@ -0,0 +1,387 @@ +package eu.siacs.conversations.services; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.provider.OpenableColumns; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.google.common.base.Charsets; +import com.google.common.base.Stopwatch; +import com.google.common.io.CountingInputStream; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipException; + +import javax.crypto.BadPaddingException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.xmpp.Jid; + +public class ImportBackupService extends Service { + + private static final int NOTIFICATION_ID = 21; + private static final AtomicBoolean running = new AtomicBoolean(false); + private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); + private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName()); + private final Set mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>()); + private DatabaseBackend mDatabaseBackend; + private NotificationManager notificationManager; + + private static int count(String input, char c) { + int count = 0; + for (char aChar : input.toCharArray()) { + if (aChar == c) { + ++count; + } + } + return count; + } + + @Override + public void onCreate() { + mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); + notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + final String password = intent.getStringExtra("password"); + final Uri data = intent.getData(); + final Uri uri; + if (data == null) { + final String file = intent.getStringExtra("file"); + uri = file == null ? null : Uri.fromFile(new File(file)); + } else { + uri = data; + } + + if (password == null || password.isEmpty() || uri == null) { + return START_NOT_STICKY; + } + if (running.compareAndSet(false, true)) { + executor.execute(() -> { + startForegroundService(); + final boolean success = importBackup(uri, password); + stopForeground(true); + running.set(false); + if (success) { + notifySuccess(); + } + stopSelf(); + }); + } else { + Log.d(Config.LOGTAG, "backup already running"); + } + return START_NOT_STICKY; + } + + public boolean getLoadingState() { + return running.get(); + } + + public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { + executor.execute(() -> { + final List accounts = mDatabaseBackend.getAccountJids(false); + final ArrayList backupFiles = new ArrayList<>(); + final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name))); + for (String app : apps) { + final File directory = new File(FileBackend.getBackupDirectory(app)); + if (!directory.exists() || !directory.isDirectory()) { + Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); + continue; + } + final File[] files = directory.listFiles(); + if (files == null) { + onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); + return; + } + for (final File file : files) { + if (file.isFile() && file.getName().endsWith(".ceb")) { + try { + final BackupFile backupFile = BackupFile.read(file); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (IOException | IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } + } + } + } + Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString())); + onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); + }); + } + + private void startForegroundService() { + startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0)); + } + + private void updateImportBackupNotification(final long total, final long current) { + final int max; + final int progress; + if (total == 0) { + max = 1; + progress = 0; + } else { + max = 100; + progress = (int) (current * 100 / total); + } + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + try { + notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress)); + } catch (final RuntimeException e) { + Log.d(Config.LOGTAG, "unable to make notification", e); + } + } + + private Notification createImportBackupNotification(final int max, final int progress) { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + mBuilder.setContentTitle(getString(R.string.restoring_backup)) + .setSmallIcon(R.drawable.ic_unarchive_white_24dp) + .setProgress(max, progress, max == 1 && progress == 0); + return mBuilder.build(); + } + + private boolean importBackup(final Uri uri, final String password) { + Log.d(Config.LOGTAG, "importing backup from " + uri); + final Stopwatch stopwatch = Stopwatch.createStarted(); + try { + final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); + final InputStream inputStream; + final String path = uri.getPath(); + final long fileSize; + if ("file".equals(uri.getScheme()) && path != null) { + final File file = new File(path); + inputStream = new FileInputStream(file); + fileSize = file.length(); + } else { + final Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); + if (returnCursor == null) { + fileSize = 0; + } else { + returnCursor.moveToFirst(); + fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE)); + returnCursor.close(); + } + inputStream = getContentResolver().openInputStream(uri); + } + if (inputStream == null) { + synchronized (mOnBackupProcessedListeners) { + for (final OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onBackupRestoreFailed(); + } + } + return false; + } + final CountingInputStream countingInputStream = new CountingInputStream(inputStream); + final DataInputStream dataInputStream = new DataInputStream(countingInputStream); + final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + Log.d(Config.LOGTAG, backupFileHeader.toString()); + + if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onAccountAlreadySetup(); + } + } + return false; + } + + final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt()); + + final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); + final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher); + + final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); + final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); + db.beginTransaction(); + String line; + StringBuilder multiLineQuery = null; + while ((line = reader.readLine()) != null) { + int count = count(line, '\''); + if (multiLineQuery != null) { + multiLineQuery.append('\n'); + multiLineQuery.append(line); + if (count % 2 == 1) { + db.execSQL(multiLineQuery.toString()); + multiLineQuery = null; + updateImportBackupNotification(fileSize, countingInputStream.getCount()); + } + } else { + if (count % 2 == 0) { + db.execSQL(line); + updateImportBackupNotification(fileSize, countingInputStream.getCount()); + } else { + multiLineQuery = new StringBuilder(line); + } + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + final Jid jid = backupFileHeader.getJid(); + final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()}); + countCursor.moveToFirst(); + final int count = countCursor.getInt(0); + Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString())); + countCursor.close(); + stopBackgroundService(); + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + l.onBackupRestored(); + } + } + return true; + } catch (final Exception e) { + final Throwable throwable = e.getCause(); + final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException; + synchronized (mOnBackupProcessedListeners) { + for (OnBackupProcessed l : mOnBackupProcessedListeners) { + if (reasonWasCrypto) { + l.onBackupDecryptionFailed(); + } else { + l.onBackupRestoreFailed(); + } + } + } + Log.d(Config.LOGTAG, "error restoring backup " + uri, e); + return false; + } + } + + private void notifySuccess() { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) + .setContentText(getString(R.string.notification_restored_backup_subtitle)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) + .setSmallIcon(R.drawable.ic_unarchive_white_24dp); + notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + } + + private void stopBackgroundService() { + Intent intent = new Intent(this, XmppConnectionService.class); + stopService(intent); + } + + public void removeOnBackupProcessedListener(OnBackupProcessed listener) { + synchronized (mOnBackupProcessedListeners) { + mOnBackupProcessedListeners.remove(listener); + } + } + + public void addOnBackupProcessedListener(OnBackupProcessed listener) { + synchronized (mOnBackupProcessedListeners) { + mOnBackupProcessedListeners.add(listener); + } + } + + @Override + public IBinder onBind(Intent intent) { + return this.binder; + } + + public interface OnBackupFilesLoaded { + void onBackupFilesLoaded(List files); + } + + public interface OnBackupProcessed { + void onBackupRestored(); + + void onBackupDecryptionFailed(); + + void onBackupRestoreFailed(); + + void onAccountAlreadySetup(); + } + + public static class BackupFile { + private final Uri uri; + private final BackupFileHeader header; + + private BackupFile(Uri uri, BackupFileHeader header) { + this.uri = uri; + this.header = header; + } + + private static BackupFile read(File file) throws IOException { + final FileInputStream fileInputStream = new FileInputStream(file); + final DataInputStream dataInputStream = new DataInputStream(fileInputStream); + BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + fileInputStream.close(); + return new BackupFile(Uri.fromFile(file), backupFileHeader); + } + + public static BackupFile read(final Context context, final Uri uri) throws IOException { + final InputStream inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + throw new FileNotFoundException(); + } + final DataInputStream dataInputStream = new DataInputStream(inputStream); + BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + inputStream.close(); + return new BackupFile(uri, backupFileHeader); + } + + public BackupFileHeader getHeader() { + return header; + } + + public Uri getUri() { + return uri; + } + } + + public class ImportBackupServiceBinder extends Binder { + public ImportBackupService getService() { + return ImportBackupService.this; + } + } +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java new file mode 100644 index 0000000000000000000000000000000000000000..b2a0d17f4acc8caa60e316cc64217e3a3604c9b7 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -0,0 +1,38 @@ +package eu.siacs.conversations.services; + +import android.content.Intent; +import android.util.Log; + +import eu.siacs.conversations.Config; + +public class QuickConversationsService extends AbstractQuickConversationsService { + + QuickConversationsService(XmppConnectionService xmppConnectionService) { + super(xmppConnectionService); + } + + @Override + public void considerSync() { + + } + + @Override + public void signalAccountStateChange() { + + } + + @Override + public boolean isSynchronizing() { + return false; + } + + @Override + public void considerSyncBackground(boolean force) { + + } + + @Override + public void handleSmsReceived(Intent intent) { + Log.d(Config.LOGTAG,"ignoring received SMS"); + } +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..fea92401b76e511d0371e91c173114dc9000426b --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java @@ -0,0 +1,151 @@ +package eu.siacs.conversations.ui; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import androidx.databinding.DataBindingUtil; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityEasyInviteBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.utils.EasyOnboardingInvite; +import eu.siacs.conversations.xmpp.Jid; + +public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested { + + private ActivityEasyInviteBinding binding; + + private EasyOnboardingInvite easyOnboardingInvite; + + + @Override + public void onCreate(final Bundle bundle) { + super.onCreate(bundle); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar(), true); + this.binding.shareButton.setOnClickListener(v -> share()); + if (bundle != null && bundle.containsKey("invite")) { + this.easyOnboardingInvite = bundle.getParcelable("invite"); + if (this.easyOnboardingInvite != null) { + showInvite(this.easyOnboardingInvite); + return; + } + } + this.showLoading(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu); + final MenuItem share = menu.findItem(R.id.action_share); + share.setVisible(easyOnboardingInvite != null); + return super.onCreateOptionsMenu(menu); + } + + public boolean onOptionsItemSelected(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.action_share) { + share(); + return true; + } else { + return super.onOptionsItemSelected(menuItem); + } + } + + private void share() { + final String shareText = getString( + R.string.easy_invite_share_text, + easyOnboardingInvite.getDomain(), + easyOnboardingInvite.getShareableLink() + ); + final Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with))); + } + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + if (easyOnboardingInvite != null) { + showInvite(easyOnboardingInvite); + } else { + showLoading(); + } + } + + private void showLoading() { + this.binding.inProgress.setVisibility(View.VISIBLE); + this.binding.invite.setVisibility(View.GONE); + } + + private void showInvite(final EasyOnboardingInvite invite) { + this.binding.inProgress.setVisibility(View.GONE); + this.binding.invite.setVisibility(View.VISIBLE); + this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain())); + final Point size = new Point(); + getWindowManager().getDefaultDisplay().getSize(size); + final int width = Math.min(size.x, size.y); + final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width); + binding.qrCode.setImageBitmap(bitmap); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + if (easyOnboardingInvite != null) { + bundle.putParcelable("invite", easyOnboardingInvite); + } + } + + @Override + void onBackendConnected() { + if (easyOnboardingInvite != null) { + return; + } + final Intent launchIntent = getIntent(); + final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT); + final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra); + if (jid == null) { + return; + } + final Account account = xmppConnectionService.findAccountByJid(jid); + xmppConnectionService.requestEasyOnboardingInvite(account, this); + } + + public static void launch(final Account account, final Activity context) { + final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + context.startActivity(intent); + } + + @Override + public void inviteRequested(EasyOnboardingInvite invite) { + this.easyOnboardingInvite = invite; + Log.d(Config.LOGTAG, "invite requested"); + refreshUi(); + } + + @Override + public void inviteRequestFailed(final String message) { + runOnUiThread(() -> { + if (!Strings.isNullOrEmpty(message)) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } + finish(); + }); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..a1f4d8d11a37ce20132442579bcfcd8fdb50c60a --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -0,0 +1,242 @@ +package eu.siacs.conversations.ui; + +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.databinding.DataBindingUtil; + +import com.google.android.material.snackbar.Snackbar; + +import java.io.IOException; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityImportBackupBinding; +import eu.siacs.conversations.databinding.DialogEnterPasswordBinding; +import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.ui.adapter.BackupFileAdapter; +import eu.siacs.conversations.utils.ThemeHelper; + +public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { + + private ActivityImportBackupBinding binding; + + private BackupFileAdapter backupFileAdapter; + private ImportBackupService service; + + private boolean mLoadingState = false; + + private int mTheme; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + this.mTheme = ThemeHelper.find(this); + setTheme(this.mTheme); + super.onCreate(savedInstanceState); + binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup); + setSupportActionBar(binding.toolbar); + setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false)); + this.backupFileAdapter = new BackupFileAdapter(); + this.binding.list.setAdapter(this.backupFileAdapter); + this.backupFileAdapter.setOnItemClickedListener(this); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.import_backup, menu); + final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file); + openBackup.setVisible(!this.mLoadingState); + return true; + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putBoolean("loading_state", this.mLoadingState); + super.onSaveInstanceState(bundle); + } + + @Override + public void onStart() { + super.onStart(); + final int theme = ThemeHelper.find(this); + if (this.mTheme != theme) { + recreate(); + } else { + bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); + } + final Intent intent = getIntent(); + if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) { + Uri uri = intent.getData(); + if (uri != null) { + openBackupFileFromUri(uri, true); + } + } + } + + @Override + public void onStop() { + super.onStop(); + if (this.service != null) { + this.service.removeOnBackupProcessedListener(this); + } + unbindService(this); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service; + this.service = binder.getService(); + this.service.addOnBackupProcessedListener(this); + setLoadingState(this.service.getLoadingState()); + this.service.loadBackupFiles(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + this.service = null; + } + + @Override + public void onBackupFilesLoaded(final List files) { + runOnUiThread(() -> backupFileAdapter.setFiles(files)); + } + + @Override + public void onClick(final ImportBackupService.BackupFile backupFile) { + showEnterPasswordDialog(backupFile, false); + } + + private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) { + try { + final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); + showEnterPasswordDialog(backupFile, finishOnCancel); + } catch (final IOException | IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); + Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); + } + } + + private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) { + final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); + Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri()); + enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString())); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setView(enterPasswordBinding.getRoot()); + builder.setTitle(R.string.enter_password); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> { + if (finishOnCancel) { + finish(); + } + }); + builder.setPositiveButton(R.string.restore, null); + builder.setCancelable(false); + final AlertDialog dialog = builder.create(); + dialog.setOnShowListener((d) -> { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { + final String password = enterPasswordBinding.accountPassword.getEditableText().toString(); + if (password.isEmpty()) { + enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password)); + return; + } + final Uri uri = backupFile.getUri(); + Intent intent = new Intent(this, ImportBackupService.class); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra("password", password); + if ("file".equals(uri.getScheme())) { + intent.putExtra("file", uri.getPath()); + } else { + intent.setData(uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + setLoadingState(true); + ContextCompat.startForegroundService(this, intent); + d.dismiss(); + }); + }); + dialog.show(); + } + + private void setLoadingState(final boolean loadingState) { + binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); + binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); + setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); + configureActionBar(getSupportActionBar(), !loadingState); + this.mLoadingState = loadingState; + invalidateOptionsMenu(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + if (resultCode == RESULT_OK) { + if (requestCode == 0xbac) { + openBackupFileFromUri(intent.getData(), false); + } + } + } + + @Override + public void onAccountAlreadySetup() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public void onBackupRestored() { + runOnUiThread(() -> { + Intent intent = new Intent(this, ConversationActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }); + } + + @Override + public void onBackupDecryptionFailed() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public void onBackupRestoreFailed() { + runOnUiThread(() -> { + setLoadingState(false); + Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show(); + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_open_backup_file) { + openBackupFile(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void openBackupFile() { + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..3419d8fc95638f975ecf630e46cc442ca92ee652 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -0,0 +1,164 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Toast; + +import androidx.databinding.DataBindingUtil; + +import java.security.SecureRandom; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.MagicCreateBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.InstallReferrerUtils; +import eu.siacs.conversations.xmpp.Jid; + +public class MagicCreateActivity extends XmppActivity implements TextWatcher { + + public static final String EXTRA_DOMAIN = "domain"; + public static final String EXTRA_PRE_AUTH = "pre_auth"; + public static final String EXTRA_USERNAME = "username"; + + private MagicCreateBinding binding; + private String domain; + private String username; + private String preAuth; + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + final Intent data = getIntent(); + this.domain = data == null ? null : data.getStringExtra(EXTRA_DOMAIN); + this.preAuth = data == null ? null : data.getStringExtra(EXTRA_PRE_AUTH); + this.username = data == null ? null : data.getStringExtra(EXTRA_USERNAME); + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create); + setSupportActionBar(this.binding.toolbar); + configureActionBar(getSupportActionBar(), this.domain == null); + if (username != null && domain != null) { + binding.title.setText(R.string.your_server_invitation); + binding.instructions.setText(getString(R.string.magic_create_text_fixed, domain)); + binding.finePrint.setVisibility(View.INVISIBLE); + binding.username.setEnabled(false); + binding.username.setText(this.username); + updateFullJidInformation(this.username); + } else if (domain != null) { + binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain)); + binding.finePrint.setVisibility(View.INVISIBLE); + } + binding.createAccount.setOnClickListener(v -> { + try { + final String username = binding.username.getText().toString(); + final Jid jid; + final boolean fixedUsername; + if (this.domain != null && this.username != null) { + fixedUsername = true; + jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain); + } else if (this.domain != null) { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, this.domain); + } else { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); + } + if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) { + binding.username.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); + } else { + binding.username.setError(null); + Account account = xmppConnectionService.findAccountByJid(jid); + if (account == null) { + account = new Account(jid, CryptoHelper.createPassword(new SecureRandom())); + account.setOption(Account.OPTION_REGISTER, true); + account.setOption(Account.OPTION_DISABLED, true); + account.setOption(Account.OPTION_MAGIC_CREATE, true); + account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); + if (this.preAuth != null) { + account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + } + xmppConnectionService.createAccount(account); + } + Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("init", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show(); + StartConversationActivity.addInviteUri(intent, getIntent()); + startActivity(intent); + } + } catch (IllegalArgumentException e) { + binding.username.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); + } + }); + binding.username.addTextChangedListener(this); + } + + @Override + public void onDestroy() { + InstallReferrerUtils.markInstallReferrerExecuted(this); + super.onDestroy(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(final Editable s) { + updateFullJidInformation(s.toString()); + } + + private void updateFullJidInformation(final String username) { + if (username.trim().isEmpty()) { + binding.fullJid.setVisibility(View.INVISIBLE); + } else { + try { + binding.fullJid.setVisibility(View.VISIBLE); + final Jid jid; + if (this.domain == null) { + jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); + } else { + jid = Jid.ofLocalAndDomainEscaped(username, this.domain); + } + binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString())); + } catch (IllegalArgumentException e) { + binding.fullJid.setVisibility(View.INVISIBLE); + } + } + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..c1ee451be7ac01ac8a25b3e7fd7cddedf6885750 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,428 @@ +package eu.siacs.conversations.ui; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.util.Pair; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; + +import org.openintents.openpgp.util.OpenPgpApi; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; + +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + +public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { + + private final String STATE_SELECTED_ACCOUNT = "selected_account"; + + private static final int REQUEST_IMPORT_BACKUP = 0x63fb; + + protected Account selectedAccount = null; + protected Jid selectedAccountJid = null; + + protected final List accountList = new ArrayList<>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false); + + protected Pair mPostponedActivityResult = null; + + @Override + public void onAccountUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + synchronized (this.accountList) { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setHomeButtonEnabled(this.accountList.size() > 0); + actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0); + } + invalidateOptionsMenu(); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_manage_accounts); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + if (savedInstanceState != null) { + String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT); + if (jid != null) { + try { + this.selectedAccountJid = Jid.ofEscaped(jid); + } catch (IllegalArgumentException e) { + this.selectedAccountJid = null; + } + } + } + + accountListView = findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); + registerForContextMenu(accountListView); + } + + @Override + protected void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + public void onSaveInstanceState(final Bundle savedInstanceState) { + if (selectedAccount != null) { + savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); + } + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); + AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedAccount = accountList.get(acmi.position); + if (this.selectedAccount.isEnabled()) { + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp()); + } else { + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); + menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); + } + menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toEscapedString()); + } + + @Override + void onBackendConnected() { + if (selectedAccountJid != null) { + this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid); + } + refreshUiReal(); + if (this.mPostponedActivityResult != null) { + this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); + } + if (Config.X509_VERIFICATION && this.accountList.size() == 0) { + if (mInvokedAddAccount.compareAndSet(false, true)) { + addAccountFromKey(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + MenuItem enableAll = menu.findItem(R.id.action_enable_all); + MenuItem addAccount = menu.findItem(R.id.action_add_account); + MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert); + + if (Config.X509_VERIFICATION) { + addAccount.setVisible(false); + addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + + if (!accountsLeftToEnable()) { + enableAll.setVisible(false); + } + MenuItem disableAll = menu.findItem(R.id.action_disable_all); + if (!accountsLeftToDisable()) { + disableAll.setVisible(false); + } + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mgmt_account_publish_avatar: + publishAvatar(selectedAccount); + return true; + case R.id.mgmt_account_disable: + disableAccount(selectedAccount); + return true; + case R.id.mgmt_account_enable: + enableAccount(selectedAccount); + return true; + case R.id.mgmt_account_delete: + deleteAccount(selectedAccount); + return true; + case R.id.mgmt_account_announce_pgp: + publishOpenPGPPublicKey(selectedAccount); + return true; + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (MenuDoubleTabUtil.shouldIgnoreTap()) { + return false; + } + switch (item.getItemId()) { + case R.id.action_add_account: + startActivity(new Intent(this, EditAccountActivity.class)); + break; + case R.id.action_import_backup: + if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { + startActivity(new Intent(this, ImportBackupActivity.class)); + } + break; + case R.id.action_disable_all: + disableAllAccounts(); + break; + case R.id.action_enable_all: + enableAllAccounts(); + break; + case R.id.action_add_account_with_cert: + addAccountFromKey(); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (grantResults.length > 0) { + if (allGranted(grantResults)) { + switch (requestCode) { + case REQUEST_IMPORT_BACKUP: + startActivity(new Intent(this, ImportBackupActivity.class)); + break; + } + } else { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } + } + if (writeGranted(grantResults, permissions)) { + if (xmppConnectionService != null) { + xmppConnectionService.restartFileObserver(); + } + } + } + + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish + // animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + + @Override + public void onClickTglAccountState(Account account, boolean enable) { + if (enable) { + enableAccount(account); + } else { + disableAccount(account); + } + } + + private void addAccountFromKey() { + try { + KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } + } + + private void publishAvatar(Account account) { + Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } + + private void disableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isEnabled()) { + list.add(account); + } + } + } + for (Account account : list) { + disableAccount(account); + } + } + + private boolean accountsLeftToDisable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isEnabled()) { + return true; + } + } + return false; + } + } + + private boolean accountsLeftToEnable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isEnabled()) { + return true; + } + } + return false; + } + } + + private void enableAllAccounts() { + List list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isEnabled()) { + list.add(account); + } + } + } + for (Account account : list) { + enableAccount(account); + } + } + + private void disableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, true); + if (!xmppConnectionService.updateAccount(account)) { + Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); + } + } + + private void enableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, false); + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetEverything(); + } + if (!xmppConnectionService.updateAccount(account)) { + Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); + } + } + + private void publishOpenPGPPublicKey(Account account) { + if (ManageAccountActivity.this.hasPgp()) { + announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); + } else { + this.showInstallPgpDialog(); + } + } + + private void deleteAccount(final Account account) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + (dialog, which) -> { + xmppConnectionService.deleteAccount(account); + selectedAccount = null; + if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + WelcomeActivity.launch(this); + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (xmppConnectionServiceBound) { + if (requestCode == REQUEST_CHOOSE_PGP_ID) { + if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { + selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID)); + announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); + } else { + choosePgpSignId(selectedAccount); + } + } else if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished); + } + this.mPostponedActivityResult = null; + } else { + this.mPostponedActivityResult = new Pair<>(requestCode, data); + } + } + } + + @Override + public void alias(final String alias) { + if (alias != null) { + xmppConnectionService.createAccountFromKey(alias, this); + } + } + + @Override + public void onAccountCreated(final Account account) { + final Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("init", true); + startActivity(intent); + } + + @Override + public void informUser(final int r) { + runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show()); + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..06320d33deea510e9edd2b139757ce2ec8c08373 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/PickServerActivity.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityPickServerBinding; +import eu.siacs.conversations.entities.Account; + +public class PickServerActivity extends XmppActivity { + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == android.R.id.home) { + startActivity(new Intent(this, WelcomeActivity.class)); + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + startActivity(new Intent(this, WelcomeActivity.class)); + super.onBackPressed(); + } + + @Override + public void onNewIntent(Intent intent) { + if (intent != null) { + setIntent(intent); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + binding.useCim.setOnClickListener(v -> { + final Intent intent = new Intent(this, MagicCreateActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + addInviteUri(intent); + startActivity(intent); + }); + binding.useOwnProvider.setOnClickListener(v -> { + List accounts = xmppConnectionService.getAccounts(); + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, true); + if (accounts.size() == 1) { + intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); + intent.putExtra("init", true); + } else if (accounts.size() >= 1) { + intent = new Intent(this, ManageAccountActivity.class); + } + addInviteUri(intent); + startActivity(intent); + }); + + } + + public void addInviteUri(Intent intent) { + StartConversationActivity.addInviteUri(intent, getIntent()); + } + + public static void launch(AppCompatActivity activity) { + Intent intent = new Intent(activity, PickServerActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(0, 0); + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..762dfbb422780c20293eec13c3acdcd846d2595e --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java @@ -0,0 +1,90 @@ +package eu.siacs.conversations.ui; + +import android.os.Bundle; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import eu.siacs.conversations.xmpp.Jid; + +public class ShareViaAccountActivity extends XmppActivity { + public static final String EXTRA_CONTACT = "contact"; + public static final String EXTRA_BODY = "body"; + + protected final List accountList = new ArrayList<>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + + @Override + protected void refreshUiReal() { + synchronized (this.accountList) { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + } + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_manage_accounts); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + accountListView = findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList, false); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener((arg0, view, position, arg3) -> { + final Account account = accountList.get(position); + final String body = getIntent().getStringExtra(EXTRA_BODY); + + try { + final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); + final Conversation conversation = xmppConnectionService.findOrCreateConversation( + account, contact, false, false); + switchToConversation(conversation, body); + } catch (IllegalArgumentException e) { + // ignore error + } + + finish(); + }); + } + + @Override + protected void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } + + @Override + void onBackendConnected() { + final int numAccounts = xmppConnectionService.getAccounts().size(); + + if (numAccounts == 1) { + final String body = getIntent().getStringExtra(EXTRA_BODY); + final Account account = xmppConnectionService.getAccounts().get(0); + + try { + final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); + final Conversation conversation = xmppConnectionService.findOrCreateConversation( + account, contact, false, false); + switchToConversation(conversation, body); + } catch (IllegalArgumentException e) { + // ignore error + } + + finish(); + } else { + refreshUiReal(); + } + } +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..8f652ce8e522bef41e4631c504aaacf3c22647cf --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -0,0 +1,234 @@ +package eu.siacs.conversations.ui; + +import android.Manifest; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.net.Uri; +import android.os.Bundle; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import java.util.Arrays; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityWelcomeBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Compatibility; +import eu.siacs.conversations.utils.InstallReferrerUtils; +import eu.siacs.conversations.utils.SignupUtils; +import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xmpp.Jid; + +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + +public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { + + private static final int REQUEST_IMPORT_BACKUP = 0x63fb; + + private XmppUri inviteUri; + + public static void launch(AppCompatActivity activity) { + Intent intent = new Intent(activity, WelcomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + activity.startActivity(intent); + activity.overridePendingTransition(0, 0); + } + + public void onInstallReferrerDiscovered(final Uri referrer) { + Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer); + if ("xmpp".equalsIgnoreCase(referrer.getScheme())) { + final XmppUri xmppUri = new XmppUri(referrer); + runOnUiThread(() -> processXmppUri(xmppUri)); + } else { + Log.i(Config.LOGTAG, "install referrer was not an XMPP uri"); + } + } + + private void processXmppUri(final XmppUri xmppUri) { + if (!xmppUri.isValidJid()) { + return; + } + final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); + final Jid jid = xmppUri.getJid(); + final Intent intent; + if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { + intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); + } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { + intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); + intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); + } else { + intent = null; + } + if (intent != null) { + startActivity(intent); + finish(); + return; + } + this.inviteUri = xmppUri; + } + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + new InstallReferrerUtils(this); + } + + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void onNewIntent(Intent intent) { + if (intent != null) { + setIntent(intent); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + if (getResources().getBoolean(R.bool.portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + super.onCreate(savedInstanceState); + ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar(), false); + binding.registerNewAccount.setOnClickListener(v -> { + final Intent intent = new Intent(this, PickServerActivity.class); + addInviteUri(intent); + startActivity(intent); + }); + binding.useExisting.setOnClickListener(v -> { + final List accounts = xmppConnectionService.getAccounts(); + Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class); + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false); + if (accounts.size() == 1) { + intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); + intent.putExtra("init", true); + } else if (accounts.size() >= 1) { + intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class); + } + addInviteUri(intent); + startActivity(intent); + }); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.welcome_menu, menu); + final MenuItem scan = menu.findItem(R.id.action_scan_qr_code); + scan.setVisible(Compatibility.hasFeatureCamera(this)); + return super.onCreateOptionsMenu(menu); + } + + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_import_backup: + if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { + startActivity(new Intent(this, ImportBackupActivity.class)); + } + break; + case R.id.action_scan_qr_code: + UriHandlerActivity.scan(this, true); + break; + case R.id.action_add_account_with_cert: + addAccountFromKey(); + break; + } + return super.onOptionsItemSelected(item); + } + + private void addAccountFromKey() { + try { + KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } + } + + @Override + public void alias(final String alias) { + if (alias != null) { + xmppConnectionService.createAccountFromKey(alias, this); + } + } + + @Override + public void onAccountCreated(final Account account) { + final Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); + intent.putExtra("init", true); + addInviteUri(intent); + startActivity(intent); + } + + @Override + public void informUser(final int r) { + runOnUiThread(() -> Toast.makeText(this, r, Toast.LENGTH_LONG).show()); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); + if (grantResults.length > 0) { + if (allGranted(grantResults)) { + switch (requestCode) { + case REQUEST_IMPORT_BACKUP: + startActivity(new Intent(this, ImportBackupActivity.class)); + break; + } + } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); + } + } + if (writeGranted(grantResults, permissions)) { + if (xmppConnectionService != null) { + xmppConnectionService.restartFileObserver(); + } + } + } + + public void addInviteUri(Intent to) { + final Intent from = getIntent(); + if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) { + final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI); + to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite); + } else if (this.inviteUri != null) { + Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow"); + to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString()); + } + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..9857dcd8ac34e664c38f8f68a99366d9f139bfe5 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java @@ -0,0 +1,170 @@ +package eu.siacs.conversations.ui.adapter; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.text.format.DateUtils; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.AccountRowBinding; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.Jid; + +public class BackupFileAdapter extends RecyclerView.Adapter { + + private OnItemClickedListener listener; + + private final List files = new ArrayList<>(); + + + @NonNull + @Override + public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false)); + } + + @Override + public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) { + final ImportBackupService.BackupFile backupFile = files.get(position); + final BackupFileHeader header = backupFile.getHeader(); + backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString()); + backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s",header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR))); + backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE); + backupFileViewHolder.binding.getRoot().setOnClickListener(v -> { + if (listener != null) { + listener.onClick(backupFile); + } + }); + loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage); + } + + @Override + public int getItemCount() { + return files.size(); + } + + public void setFiles(List files) { + this.files.clear(); + this.files.addAll(files); + notifyDataSetChanged(); + } + + public void setOnItemClickedListener(OnItemClickedListener listener) { + this.listener = listener; + } + + static class BackupFileViewHolder extends RecyclerView.ViewHolder { + private final AccountRowBinding binding; + + BackupFileViewHolder(AccountRowBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + } + + public interface OnItemClickedListener { + void onClick(ImportBackupService.BackupFile backupFile); + } + + static class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Jid jid = null; + private final int size; + + BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics(); + this.size = ((int) (48 * metrics.density)); + } + + @Override + protected Bitmap doInBackground(Jid... params) { + this.jid = params[0]; + return AvatarService.get(this.jid, size); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && !isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + private void loadAvatar(Jid jid, ImageView imageView) { + if (cancelPotentialWork(jid, imageView)) { + imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(jid); + } catch (final RejectedExecutionException ignored) { + } + } + } + + private static boolean cancelPotentialWork(Jid jid, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Jid oldJid = bitmapWorkerTask.jid; + if (oldJid == null || jid != oldJid) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + +} \ No newline at end of file diff --git a/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..593291d95374f4be2f1722b8c46a03fe34ed28dc --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/utils/ProvisioningUtils.java @@ -0,0 +1,43 @@ +package eu.siacs.conversations.utils; + +import android.app.Activity; +import android.content.Intent; +import android.widget.Toast; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.AccountConfiguration; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditAccountActivity; +import eu.siacs.conversations.xmpp.Jid; + +public class ProvisioningUtils { + + public static void provision(final Activity activity, final String json) { + final AccountConfiguration accountConfiguration; + try { + accountConfiguration = AccountConfiguration.parse(json); + } catch (final IllegalArgumentException e) { + Toast.makeText(activity, R.string.improperly_formatted_provisioning, Toast.LENGTH_LONG).show(); + return; + } + final Jid jid = accountConfiguration.getJid(); + final List accounts = DatabaseBackend.getInstance(activity).getAccountJids(true); + if (accounts.contains(jid)) { + Toast.makeText(activity, R.string.account_already_exists, Toast.LENGTH_LONG).show(); + return; + } + final Intent serviceIntent = new Intent(activity, XmppConnectionService.class); + serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT); + serviceIntent.putExtra("address", jid.asBareJid().toEscapedString()); + serviceIntent.putExtra("password", accountConfiguration.password); + Compatibility.startService(activity, serviceIntent); + final Intent intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", jid.asBareJid().toEscapedString()); + intent.putExtra("init", true); + activity.startActivity(intent); + } + +} diff --git a/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..fb088234a24e10ea315bafebd910e28df9caa270 --- /dev/null +++ b/src/cheogram/java/eu/siacs/conversations/utils/SignupUtils.java @@ -0,0 +1,77 @@ +package eu.siacs.conversations.utils; + +import android.app.Activity; +import android.content.Intent; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.ui.EditAccountActivity; +import eu.siacs.conversations.ui.MagicCreateActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.ui.PickServerActivity; +import eu.siacs.conversations.ui.StartConversationActivity; +import eu.siacs.conversations.ui.WelcomeActivity; +import eu.siacs.conversations.xmpp.Jid; + +public class SignupUtils { + + public static boolean isSupportTokenRegistry() { + return true; + } + + public static Intent getTokenRegistrationIntent(final Activity activity, Jid jid, String preAuth) { + final Intent intent = new Intent(activity, MagicCreateActivity.class); + if (jid.isDomainJid()) { + intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString()); + } else { + intent.putExtra(MagicCreateActivity.EXTRA_DOMAIN, jid.getDomain().toEscapedString()); + intent.putExtra(MagicCreateActivity.EXTRA_USERNAME, jid.getEscapedLocal()); + } + intent.putExtra(MagicCreateActivity.EXTRA_PRE_AUTH, preAuth); + return intent; + } + + public static Intent getSignUpIntent(final Activity activity) { + return getSignUpIntent(activity, false); + } + + public static Intent getSignUpIntent(final Activity activity, final boolean toServerChooser) { + final Intent intent; + if (toServerChooser) { + intent = new Intent(activity, PickServerActivity.class); + } else { + intent = new Intent(activity, WelcomeActivity.class); + } + return intent; + } + + public static Intent getRedirectionIntent(final ConversationsActivity activity) { + final XmppConnectionService service = activity.xmppConnectionService; + Account pendingAccount = AccountUtils.getPendingAccount(service); + Intent intent; + if (pendingAccount != null) { + intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", pendingAccount.getJid().asBareJid().toString()); + if (!pendingAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, pendingAccount.isOptionSet(Account.OPTION_REGISTER)); + } + } else { + if (service.getAccounts().size() == 0) { + if (Config.X509_VERIFICATION) { + intent = new Intent(activity, ManageAccountActivity.class); + } else if (Config.MAGIC_CREATE_DOMAIN != null) { + intent = getSignUpIntent(activity); + } else { + intent = new Intent(activity, EditAccountActivity.class); + } + } else { + intent = new Intent(activity, StartConversationActivity.class); + } + } + intent.putExtra("init", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } +} \ No newline at end of file diff --git a/src/cheogram/new_launcher-web.png b/src/cheogram/new_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..76057f9fea7b2eded734a58069a67c575d466167 Binary files /dev/null and b/src/cheogram/new_launcher-web.png differ diff --git a/src/cheogram/res/drawable-hdpi/ic_notification.png b/src/cheogram/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..32325e5f79569e3034e4f7ddee5fd07978b85a86 Binary files /dev/null and b/src/cheogram/res/drawable-hdpi/ic_notification.png differ diff --git a/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..18730f12f8b7a94fc49b26a916aa877091d3a9c2 Binary files /dev/null and b/src/cheogram/res/drawable-hdpi/ic_unarchive_white_24dp.png differ diff --git a/src/cheogram/res/drawable-mdpi/ic_notification.png b/src/cheogram/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..379720e94faef2a64ae5ec6668eb99fe908d3f40 Binary files /dev/null and b/src/cheogram/res/drawable-mdpi/ic_notification.png differ diff --git a/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8ec62cd34b2c63234ff5593637d5105bdb253343 Binary files /dev/null and b/src/cheogram/res/drawable-mdpi/ic_unarchive_white_24dp.png differ diff --git a/src/cheogram/res/drawable-xhdpi/ic_notification.png b/src/cheogram/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..e14da5dc66a114f24da41884ee6675c3cb2a5f8c Binary files /dev/null and b/src/cheogram/res/drawable-xhdpi/ic_notification.png differ diff --git a/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a1509a16df382eb674cddbaf52e6e70292b30c Binary files /dev/null and b/src/cheogram/res/drawable-xhdpi/ic_unarchive_white_24dp.png differ diff --git a/src/cheogram/res/drawable-xxhdpi/ic_notification.png b/src/cheogram/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..6adbc8f648ad35bc7176b6ac5e6c989634068647 Binary files /dev/null and b/src/cheogram/res/drawable-xxhdpi/ic_notification.png differ diff --git a/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..20d015751f5e7a7654fecd4d1beb6ccdb1033f8b Binary files /dev/null and b/src/cheogram/res/drawable-xxhdpi/ic_unarchive_white_24dp.png differ diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_notification.png b/src/cheogram/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..65d106b8c3bb408e7385900b93ff5481e9ce6e93 Binary files /dev/null and b/src/cheogram/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a789520baa7f4b65fd73ae78a57ec50b9d645e3c Binary files /dev/null and b/src/cheogram/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png differ diff --git a/src/cheogram/res/drawable/background.xml b/src/cheogram/res/drawable/background.xml new file mode 100644 index 0000000000000000000000000000000000000000..32bc72f1c2329d73914f9930fddc38c244397ef2 --- /dev/null +++ b/src/cheogram/res/drawable/background.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/cheogram/res/drawable/bar_logo.xml b/src/cheogram/res/drawable/bar_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ce2ae9f531625c3065d1cacbd2eb378c9a612d7 --- /dev/null +++ b/src/cheogram/res/drawable/bar_logo.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/cheogram/res/drawable/ic_launcher.xml b/src/cheogram/res/drawable/ic_launcher.xml new file mode 100644 index 0000000000000000000000000000000000000000..4be18371e4589579fe65ad4763e9f399862607a2 --- /dev/null +++ b/src/cheogram/res/drawable/ic_launcher.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/res/drawable/main_logo.xml b/src/cheogram/res/drawable/main_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fb672cd3c4426fa4e6ef1209eefbd584bbd3e81 --- /dev/null +++ b/src/cheogram/res/drawable/main_logo.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cheogram/res/drawable/splash_logo.xml b/src/cheogram/res/drawable/splash_logo.xml new file mode 100644 index 0000000000000000000000000000000000000000..500d002e446e54153c4bc122f36152bdcd2ca79a --- /dev/null +++ b/src/cheogram/res/drawable/splash_logo.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/cheogram/res/layout/activity_easy_invite.xml b/src/cheogram/res/layout/activity_easy_invite.xml new file mode 100644 index 0000000000000000000000000000000000000000..8bbf11c037e00c299f152d5084093335d7f5f607 --- /dev/null +++ b/src/cheogram/res/layout/activity_easy_invite.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + +