Progress on generating IKeys

This commit is contained in:
Paul Schaub 2020-10-24 19:25:28 +02:00
parent 733f9684c7
commit c09dc77859
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
62 changed files with 1216 additions and 532 deletions

View file

@ -82,7 +82,10 @@ dependencies {
// Dagger 2 for dependency injection
implementation "com.google.dagger:dagger:$daggerVersion"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
@ -91,6 +94,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
annotationProcessor "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycleVersion"
// Android extension for rxJava
api "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
@ -110,6 +116,9 @@ dependencies {
// circular image viewer for avatars
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
// Android specific classes of Smacks API
implementation "org.igniterealtime.smack:smack-android-extensions:$smackAndroidExtensionsVersion"

View file

@ -1,149 +1,66 @@
package org.mercury_im.messenger;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.BeforeClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.xmpp.requery.entity.AccountModel;
import org.mercury_im.messenger.xmpp.requery.entity.ChatModel;
import org.mercury_im.messenger.xmpp.requery.entity.ContactModel;
import org.mercury_im.messenger.xmpp.requery.entity.EntityModel;
import org.mercury_im.messenger.xmpp.requery.entity.LastReadChatMessageRelation;
import org.mercury_im.messenger.xmpp.requery.entity.MessageModel;
import org.mercury_im.messenger.xmpp.requery.entity.Models;
import org.mercury_im.messenger.core.requery.enums.SubscriptionDirection;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.di.DaggerAndroidTestComponent;
import org.mercury_im.messenger.entity.Account;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.requery.Persistable;
import io.requery.android.sqlite.DatabaseSource;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveSupport;
import io.requery.sql.Configuration;
import io.requery.sql.EntityDataStore;
import io.requery.sql.TableCreationMode;
import javax.inject.Inject;
import io.reactivex.observers.TestObserver;
@RunWith(AndroidJUnit4.class)
public class RequeryDatabaseTest {
static ReactiveEntityStore<Persistable> dataStore;
@Inject
AccountRepository accountRepository;
@BeforeClass
public static void setup() {
Context appContext = InstrumentationRegistry.getTargetContext();
DatabaseSource source = new DatabaseSource(appContext, Models.DEFAULT,
"mercury_test_db", 1);
// use this in development mode to drop and recreate the tables on every upgrade
source.setTableCreationMode(TableCreationMode.DROP_CREATE);
source.setLoggingEnabled(true);
Configuration configuration = source.getConfiguration();
dataStore = ReactiveSupport.toReactiveStore(
new EntityDataStore<>(configuration));
@Before
public void setup() {
DaggerAndroidTestComponent.builder().build().inject(this);
}
@Test
public void databaseTest() throws InterruptedException {
AccountModel accountModel = new AccountModel();
accountModel.setJid(JidCreate.entityBareFromOrThrowUnchecked("juliet@capulet.lit"));
accountModel.setPassword("romeo0romeo");
accountModel.setRosterVersion(null);
accountModel.setEnabled(true);
Disposable accounts = dataStore.select(AccountModel.class).get().observableResult().subscribe(
accountModels -> accountModels.forEach(System.out::println));
Disposable entities = dataStore.select(EntityModel.class).get().observableResult()
.subscribeOn(Schedulers.io())
.subscribe(entityModels -> entityModels.forEach(System.out::println));
Disposable contacts = dataStore.select(ContactModel.class).get()
.observableResult().subscribeOn(Schedulers.io()).subscribe(
contactModels -> contactModels.forEach(System.out::println));
Thread.sleep(100);
dataStore.upsert(accountModel).subscribeOn(Schedulers.io())
.subscribe();
ContactModel contactModel = new ContactModel();
EntityModel entityModel = new EntityModel();
entityModel.setAccount(accountModel);
entityModel.setJid(JidCreate.entityBareFromOrThrowUnchecked("romeo@capulet.lit"));
contactModel.setEntity(entityModel);
contactModel.setRostername("Romeo");
contactModel.setSub_direction(SubscriptionDirection.both);
dataStore.insert(contactModel).blockingGet();
dataStore.select(AccountModel.ENABLED, ContactModel.ROSTERNAME)
.from(AccountModel.class)
.join(EntityModel.class).on(AccountModel.ID.eq(EntityModel.ACCOUNT_ID))
.join(ContactModel.class).on(EntityModel.ID.eq(ContactModel.ENTITY_ID))
.get().observableResult().blockingForEach(e -> e.forEach(System.out::println));
Thread.sleep(10000);
accounts.dispose();
entities.dispose();
contacts.dispose();
}
public static class ContactDetail {
private boolean enabled;
private String rostername;
}
@Test
public void test2() {
AccountModel account = new AccountModel();
account.setJid(JidCreate.entityBareFromOrThrowUnchecked("omemouser@jabberhead.tk"));
public void insertQueryDeleteAccountTest() {
Account account = new Account();
account.setEnabled(true);
account.setPassword("nöö");
account.setPassword("sw0rdf1sh");
account.setAddress("alice@wonderland.lit");
account.setRosterVersion("");
account.setHost("wonderland.lit");
account.setPort(5222);
EntityModel entity = new EntityModel();
entity.setJid(JidCreate.entityBareFromOrThrowUnchecked("jabbertest@test.test"));
entity.setAccount(account);
// Insert account into database
TestObserver<Account> testObserver = new TestObserver<>();
accountRepository.upsertAccount(account)
.subscribe(testObserver);
ContactModel contact = new ContactModel();
contact.setEntity(entity);
contact.setRostername("Olaf");
contact.setSub_direction(SubscriptionDirection.both);
contact.setSub_approved(false);
contact.setSub_pending(true);
testObserver.assertComplete();
testObserver.assertNoErrors();
testObserver.assertNoTimeout();
testObserver.assertValue(account);
dataStore.upsert(contact).blockingGet();
// delete account from database
testObserver = new TestObserver<>();
accountRepository.deleteAccount(account)
.subscribe(testObserver);
ChatModel chat = new ChatModel();
chat.setPeer(entity);
chat.setDisplayed(true);
testObserver.assertComplete();
testObserver.assertNoErrors();
testObserver.assertNoTimeout();
dataStore.upsert(chat).blockingGet();
// assert no account in database
testObserver = new TestObserver<>();
accountRepository.getAccount(account.getId()).subscribe(testObserver);
MessageModel message = new MessageModel();
message.setBody("Hallo Welt!");
message.setChat(chat);
dataStore.upsert(message).blockingGet();
LastReadChatMessageRelation lastRead = new LastReadChatMessageRelation();
lastRead.setChat(chat);
lastRead.setMessage(message);
dataStore.upsert(lastRead).blockingGet();
MessageModel message2 = new MessageModel();
message2.setChat(chat);
message2.setBody("How are you?");
dataStore.upsert(message2).blockingGet();
LastReadChatMessageRelation lastRead2 = new LastReadChatMessageRelation();
lastRead2.setChat(chat);
lastRead2.setMessage(message2);
dataStore.upsert(lastRead2).blockingGet();
testObserver.assertComplete();
testObserver.assertNoErrors();
testObserver.assertNoValues();
}
}

View file

@ -0,0 +1,21 @@
package org.mercury_im.messenger.di;
import org.mercury_im.messenger.RequeryDatabaseTest;
import org.mercury_im.messenger.data.di.MappingModule;
import org.mercury_im.messenger.data.di.RepositoryModule;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
TestDatabaseModule.class,
RepositoryModule.class,
MappingModule.class
})
public interface AndroidTestComponent {
void inject(RequeryDatabaseTest test);
}

View file

@ -0,0 +1,37 @@
package org.mercury_im.messenger.di;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import org.mercury_im.messenger.data.model.Models;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import io.requery.Persistable;
import io.requery.android.sqlite.DatabaseSource;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveSupport;
import io.requery.sql.Configuration;
import io.requery.sql.EntityDataStore;
import io.requery.sql.TableCreationMode;
@Module
public class TestDatabaseModule {
@Provides
@Singleton
public static ReactiveEntityStore<Persistable> provideDataStore() {
Context appContext = InstrumentationRegistry.getTargetContext();
DatabaseSource source = new DatabaseSource(appContext, Models.DEFAULT,
"mercury_test_db", 1);
// use this in development mode to drop and recreate the tables on every upgrade
source.setTableCreationMode(TableCreationMode.DROP_CREATE);
source.setLoggingEnabled(true);
Configuration configuration = source.getConfiguration();
return ReactiveSupport.toReactiveStore(new EntityDataStore<>(configuration));
}
}

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mercury_im.messenger">
<!-- To auto-complete the email text field in the login form with the user's emails -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
@ -18,9 +17,10 @@
android:roundIcon="@drawable/ic_mercury_icon"
android:supportsRtl="true"
android:theme="@style/Theme.Mercury">
<activity android:name=".android.ui.ikey.IkeyBackupRestoreActivity"></activity>
<activity
android:name=".android.ui.chat.ChatActivity"
android:label="Chat"/>
android:label="Chat" />
<activity
android:name=".android.ui.MainActivity"
android:label="@string/app_name">

View file

@ -5,6 +5,7 @@ import org.mercury_im.messenger.android.di.module.AndroidDatabaseModule;
import org.mercury_im.messenger.android.di.module.AndroidSchedulersModule;
import org.mercury_im.messenger.android.ui.account.detail.AndroidAccountDetailsViewModel;
import org.mercury_im.messenger.android.ui.contacts.AndroidContactListViewModel;
import org.mercury_im.messenger.android.ui.ikey.IkeyBackupCreationAndroidViewModel;
import org.mercury_im.messenger.core.di.module.OpenPgpModule;
import org.mercury_im.messenger.core.di.module.RxMercuryMessageStoreFactoryModule;
import org.mercury_im.messenger.core.di.module.RxMercuryRosterStoreFactoryModule;
@ -93,7 +94,7 @@ public interface AppComponent {
void inject(AndroidAccountDetailsViewModel accountDetailsViewModel);
//void inject(AndroidOxSecretKeyBackupRestoreViewModel viewModel);
void inject(IkeyBackupCreationAndroidViewModel ikeyBackupCreationAndroidViewModel);
// Common VMs

View file

@ -4,6 +4,7 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.ui.MercuryActivity;
@ -11,6 +12,7 @@ import org.mercury_im.messenger.android.util.ArgumentUtils;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
import lombok.Value;
@ -20,6 +22,9 @@ public class AccountDetailsActivity extends AppCompatActivity implements Mercury
private UUID accountId;
@BindView(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -32,12 +37,14 @@ public class AccountDetailsActivity extends AppCompatActivity implements Mercury
}
private void bindUiComponents() {
setContentView(R.layout.activity_fragment_container);
setContentView(R.layout.layout_top_toolbar);
ButterKnife.bind(this);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment, new AccountDetailsFragment(accountId), "account_details")
.commit();
setSupportActionBar(toolbar);
}
private Arguments getArguments(Bundle bundle) {

View file

@ -4,6 +4,9 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@ -11,6 +14,8 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
@ -19,10 +24,14 @@ import com.google.android.material.card.MaterialCardView;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.ikey.IkeyBackupCreationFragment;
import org.mercury_im.messenger.android.ui.openpgp.ToggleableFingerprintsAdapter;
import org.mercury_im.messenger.android.ui.openpgp.OpenPgpV4FingerprintFormatter;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.List;
import java.util.UUID;
import butterknife.BindView;
@ -40,6 +49,15 @@ public class AccountDetailsFragment extends Fragment {
@BindView(R.id.btn_share)
Button localFingerprintShareButton;
@BindView(R.id.btn_share_ikey)
Button ikeyFingerprintShareButton;
@BindView(R.id.btn_backup_ikey)
Button ikeyCreateBackupButton;
@BindView(R.id.ikey_fingerprint)
TextView ikeyFingerprint;
@BindView(R.id.local_fingerprint)
TextView localFingerprint;
@ -49,8 +67,11 @@ public class AccountDetailsFragment extends Fragment {
@BindView(R.id.other_fingerprints_card)
MaterialCardView otherFingerprintsLayout;
@BindView(R.id.layout_ikey)
MaterialCardView ikeyLayout;
private final UUID accountId;
private ToggleableFingerprintsAdapter adapter;
private ToggleableFingerprintsAdapter otherFingerprintsAdapter;
private AndroidAccountDetailsViewModel viewModel;
@ -58,19 +79,6 @@ public class AccountDetailsFragment extends Fragment {
this.accountId = accountId;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_account_details, container, false);
ButterKnife.bind(this, view);
externalFingerprintRecyclerView.setAdapter(adapter);
observe();
return view;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@ -79,33 +87,110 @@ public class AccountDetailsFragment extends Fragment {
viewModel = new ViewModelProvider(this, factory)
.get(AndroidAccountDetailsViewModel.class);
this.adapter = new ToggleableFingerprintsAdapter(
this.otherFingerprintsAdapter = new ToggleableFingerprintsAdapter(
(fingerprint, checked) -> viewModel.markFingerprintTrusted(fingerprint, checked));
this.adapter.setItemLongClickListener(fingerprint -> viewModel.unpublishPublicKey(fingerprint));
this.otherFingerprintsAdapter.setItemLongClickListener(fingerprint -> viewModel.unpublishPublicKey(fingerprint));
}
private void observe() {
viewModel.getLocalFingerprint().observe(getViewLifecycleOwner(),
f -> localFingerprint.setText(OpenPgpV4FingerprintFormatter.formatOpenPgpV4Fingerprint(f)));
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.menu_account_details, menu);
super.onCreateOptionsMenu(menu, inflater);
}
viewModel.getRemoteFingerprints().observe(getViewLifecycleOwner(), items -> {
otherFingerprintsLayout.setVisibility(items.isEmpty() ? View.GONE : View.VISIBLE);
adapter.setItems(items);
});
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_generate_ikey:
viewModel.onGenerateIkey();
return true;
case R.id.action_delete_ikey:
viewModel.onDeleteIkey();
return true;
case R.id.action_restore_ikey_backup:
viewModel.onRestoreIkeyBackup();
return true;
}
return super.onOptionsItemSelected(item);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_account_details, container, false);
ButterKnife.bind(this, view);
setHasOptionsMenu(true);
externalFingerprintRecyclerView.setAdapter(otherFingerprintsAdapter);
observeViewModel();
return view;
}
private void observeViewModel() {
viewModel.getIkeyFingerprint().observe(getViewLifecycleOwner(), this::displayIkeyFingerprint);
viewModel.getLocalFingerprint().observe(getViewLifecycleOwner(), this::displayLocalOxFingerprint);
viewModel.getRemoteFingerprints().observe(getViewLifecycleOwner(), this::displayOtherOxFingerprints);
viewModel.getJid().observe(getViewLifecycleOwner(), accountJid -> jid.setText(accountJid.toString()));
localFingerprintShareButton.setOnClickListener(v -> {
OpenPgpV4Fingerprint fingerprint = viewModel.getLocalFingerprint().getValue();
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "openpgp4fpr:" + fingerprint);
sendIntent.setType("text/plain");
startShareFingerprintIntent(fingerprint);
});
Intent shareIntent = Intent.createChooser(sendIntent, "Share OpenPGP Fingerprint");
startActivity(shareIntent);
ikeyFingerprintShareButton.setOnClickListener(v -> {
Optional<OpenPgpV4Fingerprint> fingerprint = viewModel.getIkeyFingerprint().getValue();
if (fingerprint == null || !fingerprint.isPresent()) {
return;
}
startShareFingerprintIntent(fingerprint.getItem());
});
ikeyCreateBackupButton.setOnClickListener(v -> {
getParentFragmentManager().beginTransaction()
.replace(R.id.fragment, IkeyBackupCreationFragment.newInstance(accountId)).commit();
});
}
private void startShareFingerprintIntent(OpenPgpV4Fingerprint fingerprint) {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "openpgp4fpr:" + fingerprint);
sendIntent.setType("text/plain");
Intent shareIntent = Intent.createChooser(sendIntent, "Share OpenPGP Fingerprint");
startActivity(shareIntent);
}
private void displayIkeyFingerprint(Optional<OpenPgpV4Fingerprint> fingerprint) {
if (fingerprint.isPresent()) {
ikeyLayout.setVisibility(View.VISIBLE);
ikeyFingerprint.setText(OpenPgpV4FingerprintFormatter.formatOpenPgpV4Fingerprint(fingerprint.getItem()));
} else {
ikeyLayout.setVisibility(View.GONE);
ikeyFingerprint.setText(null);
}
}
private void displayLocalOxFingerprint(OpenPgpV4Fingerprint fingerprint) {
if (fingerprint != null) {
localFingerprint.setText(OpenPgpV4FingerprintFormatter.formatOpenPgpV4Fingerprint(fingerprint));
} else {
localFingerprint.setText(null);
}
}
private void displayOtherOxFingerprints(List<FingerprintViewItem> fingerprints) {
otherFingerprintsAdapter.setItems(fingerprints);
if (fingerprints.isEmpty()) {
otherFingerprintsLayout.setVisibility(View.GONE);
} else {
otherFingerprintsLayout.setVisibility(View.VISIBLE);
}
}
}

View file

@ -14,6 +14,7 @@ import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.util.DefaultUtil;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.account.detail.AccountDetailsViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
@ -41,9 +42,10 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
AccountDetailsViewModel commonViewModel;
private final UUID accountId;
private MutableLiveData<OpenPgpV4Fingerprint> localFingerprint = new MutableLiveData<>(new OpenPgpV4Fingerprint("09858F60046289311743B90F3152226EB43287C5"));
private MutableLiveData<Optional<OpenPgpV4Fingerprint>> ikeyFingerprint = new MutableLiveData<>();
private MutableLiveData<OpenPgpV4Fingerprint> localFingerprint = new MutableLiveData<>();
private MutableLiveData<List<FingerprintViewItem>> remoteFingerprints = new MutableLiveData<>(new ArrayList<>());
private MutableLiveData<EntityBareJid> jid = new MutableLiveData<>(JidCreate.entityBareFromOrThrowUnchecked("placeholder@place.holder"));
private MutableLiveData<EntityBareJid> jid = new MutableLiveData<>(DefaultUtil.defaultJid());
public AndroidAccountDetailsViewModel(@NonNull Application application, UUID accountId) {
super(application);
@ -58,6 +60,11 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
.map(Optional::getItem)
.subscribe(localFingerprint::postValue));
addDisposable(getCommonViewModel().observeIkeyFingerprint(accountId)
.compose(schedulers.executeUiSafeObservable())
.subscribe(ikeyFingerprint::postValue,
e -> LOGGER.log(Level.SEVERE, "Error displaying ikey fingerprint", e)));
addDisposable(getCommonViewModel().observeRemoteFingerprints(accountId)
.compose(schedulers.executeUiSafeObservable())
.subscribe(list -> {
@ -83,6 +90,10 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
getCommonViewModel().markFingerprintTrusted(accountId, fingerprint, trusted);
}
public LiveData<Optional<OpenPgpV4Fingerprint>> getIkeyFingerprint() {
return ikeyFingerprint;
}
public LiveData<OpenPgpV4Fingerprint> getLocalFingerprint() {
return localFingerprint;
}
@ -99,6 +110,30 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
e -> LOGGER.log(Level.SEVERE, "Error unpublishing fingerprint " + fingerprint, e)));
}
public void onGenerateIkey() {
addDisposable(getCommonViewModel().generateIkey(accountId)
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "IKey generated for account " + accountId),
e -> LOGGER.log(Level.SEVERE, "Could not generate Ikey", e)));
}
public void onDeleteIkey() {
addDisposable(getCommonViewModel().deleteIkey(accountId)
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "IKey deleted for account " + accountId),
e -> LOGGER.log(Level.SEVERE, "Could not delete Ikey", e)));
}
public void onRestoreIkeyBackup() {
addDisposable(getCommonViewModel().restoreIkeyBackup(accountId)
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "IKey restored for account " + accountId),
e -> LOGGER.log(Level.SEVERE, "Could not restore Ikey backup", e)));
}
public static class AndroidAccountDetailsViewModelFactory implements ViewModelProvider.Factory {
private final Application application;

View file

@ -39,7 +39,7 @@ public class ContactDetailActivity extends AppCompatActivity implements MercuryA
}
private void bindUiComponents() {
setContentView(R.layout.activity_fragment_container);
setContentView(R.layout.layout_top_toolbar);
ButterKnife.bind(this);
getSupportFragmentManager().beginTransaction()

View file

@ -0,0 +1,80 @@
package org.mercury_im.messenger.android.ui.ikey;
import android.graphics.Bitmap;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.android.util.QrCodeGenerator;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.ikey.IkeySecretKeyBackupCreationViewModel;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Observable;
public class IkeyBackupCreationAndroidViewModel extends ViewModel implements MercuryAndroidViewModel<IkeySecretKeyBackupCreationViewModel> {
private static final Logger LOGGER = Logger.getLogger(IkeyBackupCreationAndroidViewModel.class.getName());
MutableLiveData<OpenPgpSecretKeyBackupPassphrase> passphrase = new MutableLiveData<>();
MutableLiveData<Bitmap> passphraseAsQrCode = new MutableLiveData<>();
@Inject
IkeySecretKeyBackupCreationViewModel commonViewModel;
@Inject
SchedulersFacade schedulers;
public IkeyBackupCreationAndroidViewModel() {
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public void initialize(UUID accountId) {
getCommonViewModel().setAccountId(accountId);
Observable<Optional<OpenPgpSecretKeyBackupPassphrase>> passphraseObservable =
//getCommonViewModel().getPassphrase()
Observable.just(new Optional<>(new OpenPgpSecretKeyBackupPassphrase("71ZA-Y416-UA7A-7NCE-3SNM-88EF")));
addDisposable(passphraseObservable
.subscribeOn(schedulers.getIoScheduler())
.observeOn(schedulers.getUiScheduler())
.filter(Optional::isPresent)
.map(Optional::getItem)
.subscribe(
passphrase::setValue,
e -> LOGGER.log(Level.SEVERE, "Error subscribing to passphrase", e)));
addDisposable(passphraseObservable
.filter(Optional::isPresent)
.map(Optional::getItem)
.map(pass -> QrCodeGenerator.generateBarcode(pass.toString()))
.subscribeOn(schedulers.getIoScheduler())
.observeOn(schedulers.getUiScheduler())
.subscribe(passphraseAsQrCode::setValue,
e -> LOGGER.log(Level.SEVERE, "Error subscribing to passphrase QR code", e)));
}
public LiveData<OpenPgpSecretKeyBackupPassphrase> getPassphrase() {
return passphrase;
}
public LiveData<Bitmap> getPassphraseAsQrCode() {
return passphraseAsQrCode;
}
@Override
public IkeySecretKeyBackupCreationViewModel getCommonViewModel() {
return commonViewModel;
}
}

View file

@ -0,0 +1,62 @@
package org.mercury_im.messenger.android.ui.ikey;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.mercury_im.messenger.R;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
public class IkeyBackupCreationFragment extends Fragment {
private IkeyBackupCreationAndroidViewModel viewModel;
@BindView(R.id.backup_code)
TextView backupCode;
@BindView(R.id.qr_code)
ImageView qrCode;
private final UUID accountId;
private IkeyBackupCreationFragment(UUID accountId) {
this.accountId = accountId;
}
public static IkeyBackupCreationFragment newInstance(UUID accountId) {
return new IkeyBackupCreationFragment(accountId);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ikey_backup_creation, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel = new ViewModelProvider(this).get(IkeyBackupCreationAndroidViewModel.class);
viewModel.initialize(accountId);
viewModel.getPassphrase().observe(getViewLifecycleOwner(), passphrase -> backupCode.setText(passphrase));
viewModel.getPassphraseAsQrCode().observe(getViewLifecycleOwner(), bitmap -> qrCode.setImageBitmap(bitmap));
}
}

View file

@ -0,0 +1,16 @@
package org.mercury_im.messenger.android.ui.ikey;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import org.mercury_im.messenger.R;
public class IkeyBackupRestoreActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ikey_backup_restore);
}
}

View file

@ -42,7 +42,7 @@ public class ToggleableFingerprintsAdapter extends RecyclerView.Adapter<Toggleab
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.view_fingerprint_toggleable, parent, false);
.inflate(R.layout.view_openpgp_4_fingerprint_toggleable, parent, false);
return new ViewHolder(view);
}

View file

@ -0,0 +1,20 @@
package org.mercury_im.messenger.android.util;
import android.graphics.Bitmap;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.journeyapps.barcodescanner.BarcodeEncoder;
public class QrCodeGenerator {
public static Bitmap generateBarcode(String content) throws WriterException {
MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE,200,200);
BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
Bitmap bitmap = barcodeEncoder.createBitmap(bitMatrix);
return bitmap;
}
}

View file

@ -1,19 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_layout"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
</FrameLayout>
</LinearLayout>
android:layout_height="match_parent" />

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.ui.ikey.IkeyBackupRestoreActivity">
<TextView
android:id="@+id/notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="An iKey backup has been found!"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice">
<include
layout="@layout/view_openpgp_4_fingerprint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
tools:layout_editor_absoluteY="19dp" />
</LinearLayout>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="In order to restore the backup, please enter the backup code below."
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" >
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Backup Code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imageButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="8dp"/>
<ImageButton
android:id="@+id/imageButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:padding="4dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_qr_code_scanner_black_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -14,31 +14,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
tools:menu="@menu/menu_main"/>
<include layout="@layout/view_toolbar_top" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/fragment"
<include layout="@layout/activity_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_layout"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
app:layout_constraintBottom_toTopOf="@id/bottom_navigation" />
</FrameLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_menu_main" />
<include layout="@layout/view_toolbar_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,120 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="196dp"
android:layout_height="196dp"
android:paddingTop="12dp"
android:src="@drawable/aldrin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/jid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar"
tools:text="aldrin@mercury.im" />
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical"
android:padding="12dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/jid">
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.SearchResult.Title"
android:text="Encryption Keys" />
<com.google.android.material.card.MaterialCardView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="4dp"
app:cardElevation="4dp">
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="196dp"
android:layout_height="196dp"
android:paddingTop="12dp"
android:src="@drawable/aldrin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/jid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar"
tools:text="aldrin@mercury.im" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp">
android:padding="12dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/jid">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Local Fingerprint"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.SearchResult.Title"
android:text="Encryption Keys" />
<include
android:id="@+id/local_fingerprint"
layout="@layout/view_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
<com.google.android.material.card.MaterialCardView
android:id="@+id/layout_ikey"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
android:layout_marginBottom="8dp"
android:visibility="gone"
tools:visibility="visible"
app:cardCornerRadius="4dp"
app:cardElevation="4dp">
<Button
android:id="@+id/btn_backup"
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp">
<TextView
android:id="@+id/title_ikey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Account Identity Key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/ikey_fingerprint"
layout="@layout/view_openpgp_4_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_ikey" />
<Button
android:id="@+id/btn_backup_ikey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_server_backup"
app:layout_constraintEnd_toStartOf="@+id/btn_share_ikey"
app:layout_constraintTop_toBottomOf="@+id/ikey_fingerprint" />
<Button
android:id="@+id/btn_share_ikey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="share"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ikey_fingerprint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="4dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Local Fingerprint"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/local_fingerprint"
layout="@layout/view_openpgp_4_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<Button
android:id="@+id/btn_backup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_server_backup"
app:layout_constraintEnd_toStartOf="@+id/btn_share"
app:layout_constraintTop_toBottomOf="@+id/local_fingerprint" />
<Button
android:id="@+id/btn_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="share"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/local_fingerprint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<include layout="@layout/view_fingerprints_card_toggleable"
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/other_fingerprints_card"
android:layout_height="wrap_content"
android:text="@string/btn_server_backup"
app:layout_constraintEnd_toStartOf="@+id/btn_share"
app:layout_constraintTop_toBottomOf="@+id/local_fingerprint" />
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/btn_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="share"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/local_fingerprint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<include layout="@layout/view_fingerprints_card_toggleable"
android:id="@+id/other_fingerprints_card"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</LinearLayout>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.ui.ikey.IkeyBackupCreationFragment">
<TextView
android:id="@+id/notice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.SearchResult.Title"
android:text="Copy Identity Key to new Device"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You should now write down the code below and store it somewhere safe.\nAlternatively you can scan the code below with another device to copy the identity key over."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice" />
<ImageView
android:id="@+id/qr_code"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice"
app:srcCompat="@drawable/ic_qr_code_scanner_black_24dp" />
<TextView
android:id="@+id/backup_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/qr_code"
tools:text="TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.ui.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<include layout="@layout/view_toolbar_top" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_layout"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
</FrameLayout>
<include layout="@layout/view_toolbar_bottom" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file