Compare commits

...

2 Commits

Author SHA1 Message Date
Paul Schaub c4e672cc21
Publish correct superordinate key 2020-12-18 16:33:23 +01:00
Paul Schaub 1e3c98abea
Rework ContactDetailViewModel 2020-12-18 14:37:04 +01:00
12 changed files with 453 additions and 298 deletions

View File

@ -9,6 +9,7 @@ import org.mercury_im.messenger.android.ui.account.login.EnterAccountDetailsFrag
import org.mercury_im.messenger.android.ui.account.login.AndroidIkeySetupViewModel; import org.mercury_im.messenger.android.ui.account.login.AndroidIkeySetupViewModel;
import org.mercury_im.messenger.android.ui.contacts.AndroidContactListViewModel; import org.mercury_im.messenger.android.ui.contacts.AndroidContactListViewModel;
import org.mercury_im.messenger.android.crypto.ikey.AndroidIkeyBackupCreationViewModel; import org.mercury_im.messenger.android.crypto.ikey.AndroidIkeyBackupCreationViewModel;
import org.mercury_im.messenger.android.ui.contacts.detail.AndroidContactDetailViewModel;
import org.mercury_im.messenger.core.di.module.IkeyModule; import org.mercury_im.messenger.core.di.module.IkeyModule;
import org.mercury_im.messenger.core.di.module.OpenPgpModule; 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.RxMercuryMessageStoreFactoryModule;
@ -32,7 +33,6 @@ import org.mercury_im.messenger.android.ui.chat.ChatInputViewModel;
import org.mercury_im.messenger.android.ui.chat.AndroidChatViewModel; import org.mercury_im.messenger.android.ui.chat.AndroidChatViewModel;
import org.mercury_im.messenger.android.ui.chatlist.AndroidChatListViewModel; import org.mercury_im.messenger.android.ui.chatlist.AndroidChatListViewModel;
import org.mercury_im.messenger.android.ui.contacts.detail.ContactDetailActivity; import org.mercury_im.messenger.android.ui.contacts.detail.ContactDetailActivity;
import org.mercury_im.messenger.android.ui.contacts.detail.ContactDetailViewModel;
import org.mercury_im.messenger.core.viewmodel.account.LoginViewModel; import org.mercury_im.messenger.core.viewmodel.account.LoginViewModel;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -93,7 +93,7 @@ public interface AppComponent {
void inject(AndroidChatListViewModel chatListViewModel); void inject(AndroidChatListViewModel chatListViewModel);
void inject(ContactDetailViewModel contactDetailViewModel); void inject(AndroidContactDetailViewModel contactDetailViewModel);
void inject(AccountDetailsViewModel accountDetailsViewModel); void inject(AccountDetailsViewModel accountDetailsViewModel);

View File

@ -137,6 +137,9 @@ public class AccountDetailsFragment extends Fragment {
.replace(R.id.fragment, IkeySetupFragment.newInstance(accountId)) .replace(R.id.fragment, IkeySetupFragment.newInstance(accountId))
.commit(); .commit();
return true; return true;
case R.id.action_publish_ikey_element:
sendIkeyElement();
return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }

View File

@ -0,0 +1,132 @@
package org.mercury_im.messenger.android.ui.contacts.detail;
import android.graphics.drawable.Drawable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.android.ui.base.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.contact.ContactDetailViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Single;
public class AndroidContactDetailViewModel extends ViewModel implements MercuryAndroidViewModel<ContactDetailViewModel> {
@Inject
ContactDetailViewModel commonViewModel;
private MutableLiveData<UUID> contactAccountId = new MutableLiveData<>(UUID.randomUUID());
private MutableLiveData<String> contactAddress = new MutableLiveData<>("alice@wonderland.lit");
private MutableLiveData<Drawable> contactAvatar = new MutableLiveData<>(new AvatarDrawable("Alice Wonderland", "alice@wonderland.lit"));
private MutableLiveData<Presence.Mode> contactPresenceMode = new MutableLiveData<>(Presence.Mode.available);
private MutableLiveData<String> contactPresenceStatus = new MutableLiveData<>("Going down the rabbit hole.");
private MutableLiveData<String> contactName = new MutableLiveData<>("Alice Wonderland");
private MutableLiveData<String> contactAccountAddress = new MutableLiveData<>("mad@hatter.lit");
private MutableLiveData<List<String>> contactGroups = new MutableLiveData<>(Collections.emptyList());
private MutableLiveData<List<FingerprintViewItem>> contactFingerprints = new MutableLiveData<>(Collections.emptyList());
private MutableLiveData<Optional<FingerprintViewItem>> contactIkeyFingerprint = new MutableLiveData<>(new Optional<>());
public AndroidContactDetailViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public Completable init(UUID peerId) {
return getCommonViewModel().init(peerId)
.andThen(Completable.fromAction(() -> {
addDisposable(commonViewModel.getContactAccountAddress()
.map(Objects::toString)
.subscribe(contactAccountAddress::postValue));
addDisposable(commonViewModel.getContactAddress()
.map(Objects::toString)
.subscribe(contactAddress::postValue));
addDisposable(commonViewModel.getContactDisplayName().subscribe(contactName::postValue));
addDisposable(commonViewModel.getContactPresenceMode().subscribe(contactPresenceMode::postValue));
addDisposable(commonViewModel.getContactPresenceStatus().subscribe(contactPresenceStatus::postValue));
addDisposable(commonViewModel.getContactGroups().subscribe(contactGroups::postValue));
addDisposable(commonViewModel.getContactAvatarBase()
.map(tuple -> new AvatarDrawable(tuple.getFirst(), tuple.getSecond().toString()))
.subscribe(contactAvatar::postValue));
addDisposable(commonViewModel.getContactIdentityFingerprint().subscribe(contactIkeyFingerprint::postValue));
addDisposable(commonViewModel.getContactDeviceFingerprints().subscribe(contactFingerprints::postValue));
}));
}
public LiveData<String> getContactAddress() {
return contactAddress;
}
public LiveData<UUID> getAccountId() {
return contactAccountId;
}
public LiveData<Drawable> getContactAvatar() {
return contactAvatar;
}
public LiveData<Presence.Mode> getContactPresenceMode() {
return contactPresenceMode;
}
public LiveData<String> getContactName() {
return contactName;
}
public LiveData<String> getContactPresenceStatus() {
return contactPresenceStatus;
}
public LiveData<String> getContactAccountAddress() {
return contactAccountAddress;
}
public LiveData<List<String>> getContactGroups() {
return contactGroups;
}
public LiveData<Optional<FingerprintViewItem>> getContactIdentityFingerprint() {
return contactIkeyFingerprint;
}
public LiveData<List<FingerprintViewItem>> getContactDeviceFingerprints() {
return contactFingerprints;
}
public Single<UUID> getOrCreateChat() {
return commonViewModel.getOrCreateChat();
}
public void changeContactName(String newName)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
commonViewModel.changeContactName(newName);
}
public void markDeviceFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, boolean checked) {
commonViewModel.markDeviceFingerprintTrusted(fingerprint, checked);
}
@Override
public ContactDetailViewModel getCommonViewModel() {
return commonViewModel;
}
}

View File

@ -8,20 +8,30 @@ import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.android.MercuryImApplication; import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.R; import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.di.module.AndroidSchedulersModule;
import org.mercury_im.messenger.android.ui.base.MercuryActivity; import org.mercury_im.messenger.android.ui.base.MercuryActivity;
import org.mercury_im.messenger.android.util.ArgumentUtils; import org.mercury_im.messenger.android.util.ArgumentUtils;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
import java.util.UUID; import java.util.UUID;
import javax.inject.Inject;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import lombok.Value; import lombok.Value;
public class ContactDetailActivity extends AppCompatActivity implements MercuryActivity { public class ContactDetailActivity extends AppCompatActivity implements MercuryActivity {
public static final String EXTRA_PEER_ID = "PEER_ID"; public static final String EXTRA_PEER_ID = "PEER_ID";
private ContactDetailViewModel androidContactDetailViewModel; private AndroidContactDetailViewModel androidContactDetailViewModel;
private UUID peerId; private UUID peerId;
Disposable disposable;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -30,10 +40,13 @@ public class ContactDetailActivity extends AppCompatActivity implements MercuryA
Arguments arguments = getArguments(savedInstanceState); Arguments arguments = getArguments(savedInstanceState);
peerId = arguments.getPeerId(); peerId = arguments.getPeerId();
androidContactDetailViewModel = new ViewModelProvider(this).get(ContactDetailViewModel.class); androidContactDetailViewModel = new ViewModelProvider(this).get(AndroidContactDetailViewModel.class);
androidContactDetailViewModel.init(peerId);
bindUiComponents(); bindUiComponents();
disposable = androidContactDetailViewModel.init(peerId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
} }
private void bindUiComponents() { private void bindUiComponents() {
@ -55,4 +68,10 @@ public class ContactDetailActivity extends AppCompatActivity implements MercuryA
private class Arguments { private class Arguments {
UUID peerId; UUID peerId;
} }
@Override
protected void onDestroy() {
super.onDestroy();
disposable.dispose();
}
} }

View File

@ -82,7 +82,6 @@ public class ContactDetailFragment extends Fragment {
@BindView(R.id.card_contact_fingerprints) @BindView(R.id.card_contact_fingerprints)
MaterialCardView fingerprintsLayout; MaterialCardView fingerprintsLayout;
private ContactDetailViewModel viewModel;
@BindView(R.id.card_ikey_fingerprint) @BindView(R.id.card_ikey_fingerprint)
MaterialCardView ikeyLayout; MaterialCardView ikeyLayout;
@ -90,6 +89,7 @@ public class ContactDetailFragment extends Fragment {
TextView ikeyFingerprint; TextView ikeyFingerprint;
private ToggleableFingerprintsAdapter fingerprintsAdapter; private ToggleableFingerprintsAdapter fingerprintsAdapter;
private AndroidContactDetailViewModel viewModel;
@Nullable @Nullable
@Override @Override
@ -107,17 +107,9 @@ public class ContactDetailFragment extends Fragment {
contactName.setOnClickListener(v -> displayChangeContactNameDialog()); contactName.setOnClickListener(v -> displayChangeContactNameDialog());
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.addContactToRosterGroup().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
});
fingerprintsAdapter = new ToggleableFingerprintsAdapter( fingerprintsAdapter = new ToggleableFingerprintsAdapter(
(fingerprint, checked) -> viewModel.markFingerprintTrusted(fingerprint, checked)); (fingerprint, checked) -> viewModel.markDeviceFingerprintTrusted(fingerprint, checked));
fingerprintRecyclerView.setAdapter(fingerprintsAdapter); fingerprintRecyclerView.setAdapter(fingerprintsAdapter);
return view; return view;
@ -142,7 +134,7 @@ public class ContactDetailFragment extends Fragment {
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
viewModel = new ViewModelProvider((ViewModelStoreOwner) context).get(ContactDetailViewModel.class); viewModel = new ViewModelProvider((ViewModelStoreOwner) context).get(AndroidContactDetailViewModel.class);
observeViewModel(); observeViewModel();
} }
@ -171,8 +163,8 @@ public class ContactDetailFragment extends Fragment {
viewModel.getContactPresenceStatus().observe(this, presenceText -> contactPresence.setText(presenceText)); viewModel.getContactPresenceStatus().observe(this, presenceText -> contactPresence.setText(presenceText));
viewModel.getContactAccountAddress().observe(this, address -> contactAccount.setText(address)); viewModel.getContactAccountAddress().observe(this, address -> contactAccount.setText(address));
viewModel.getContactGroups().observe(this, this::setRosterGroups); viewModel.getContactGroups().observe(this, this::setRosterGroups);
viewModel.getContactFingerprints().observe(this, this::setFingerprints); viewModel.getContactDeviceFingerprints().observe(this, this::setFingerprints);
viewModel.getContactIkeyFingerprint().observe(this, this::setIkeyFingerprint); viewModel.getContactIdentityFingerprint().observe(this, this::setIkeyFingerprint);
} }
private void setRosterGroups(List<String> groups) { private void setRosterGroups(List<String> groups) {
@ -180,15 +172,6 @@ public class ContactDetailFragment extends Fragment {
for (String group : groups) { for (String group : groups) {
Chip chip = new Chip(contactGroups.getContext()); Chip chip = new Chip(contactGroups.getContext());
chip.setText(group); chip.setText(group);
chip.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
viewModel.removeContactFromRosterGroup(group)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe();
return true;
}
});
contactGroups.addView(chip); contactGroups.addView(chip);
} }
} }

View File

@ -1,258 +0,0 @@
package org.mercury_im.messenger.android.ui.contacts.detail;
import android.graphics.drawable.Drawable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.PresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterGroup;
import org.jivesoftware.smackx.ikey.util.IkeyTrust;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.core.Messenger;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.DirectChatRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.android.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.core.util.CombinedPresenceListener;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
public class ContactDetailViewModel extends ViewModel {
@Inject
PeerRepository peerRepository;
@Inject
DirectChatRepository directChatRepository;
@Inject
OpenPgpRepository openPgpRepository;
@Inject
IkeyRepository ikeyRepository;
@Inject
SchedulersFacade schedulers;
@Inject
Messenger messenger;
private MutableLiveData<UUID> contactAccountId = new MutableLiveData<>(UUID.randomUUID());
private MutableLiveData<String> contactAddress = new MutableLiveData<>("alice@wonderland.lit");
private MutableLiveData<Drawable> contactAvatar = new MutableLiveData<>(new AvatarDrawable("Alice Wonderland", "alice@wonderland.lit"));
private MutableLiveData<Presence.Mode> contactPresenceMode = new MutableLiveData<>(Presence.Mode.available);
private MutableLiveData<String> contactPresenceStatus = new MutableLiveData<>("Going down the rabbit hole.");
private MutableLiveData<String> contactName = new MutableLiveData<>("Alice Wonderland");
private MutableLiveData<String> contactAccountAddress = new MutableLiveData<>("mad@hatter.lit");
private MutableLiveData<List<String>> contactGroups = new MutableLiveData<>(Collections.emptyList());
private MutableLiveData<List<FingerprintViewItem>> contactFingerprints = new MutableLiveData<>(Collections.emptyList());
private MutableLiveData<Optional<FingerprintViewItem>> contactIkeyFingerprint = new MutableLiveData<>(new Optional<>());
private Roster roster;
private UUID peerId;
private CompositeDisposable disposable = new CompositeDisposable();
public ContactDetailViewModel() {
super();
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public void init(UUID peerId) {
this.peerId = peerId;
disposable.add(peerRepository.getPeer(peerId)
.subscribe(p -> {
roster = Roster.getInstanceFor(messenger.getConnectionManager().getConnection(p.getAccount()).getConnection());
roster.addPresenceEventListener(presenceEventListener);
Presence presence = roster.getPresence(p.getJid());
if (presence != null) {
contactPresenceMode.postValue(presence.getMode());
contactPresenceStatus.postValue(presence.getStatus());
}
disposable.add(openPgpRepository.observeFingerprints(p.getAccount().getId(), p.getJid())
.subscribeOn(schedulers.getIoScheduler())
.observeOn(schedulers.getUiScheduler())
.subscribe(list -> contactFingerprints.setValue(list)));
}));
disposable.add(peerRepository.observePeer(peerId)
.filter(Optional::isPresent)
.map(Optional::getItem)
.compose(schedulers.executeUiSafeObservable())
.subscribe(peer -> {
contactAddress.setValue(peer.getAddress());
contactAccountId.setValue(peer.getAccount().getId());
contactAccountAddress.setValue(peer.getAccount().getAddress());
contactAvatar.setValue(new AvatarDrawable(peer.getDisplayName(), peer.getAddress()));
contactName.setValue(peer.getDisplayName());
RosterEntry entry = roster.getEntry(peer.getJid());
if (entry != null) {
List<RosterGroup> groups = entry.getGroups();
List<String> groupNames = new ArrayList<>(groups.size());
for (RosterGroup g : groups) {
groupNames.add(g.getName());
}
contactGroups.postValue(groupNames);
}
}));
disposable.add(ikeyRepository.loadRecord(
getAccountId().getValue(),
JidCreate.entityBareFromOrThrowUnchecked(getContactAccountAddress().getValue()))
.compose(schedulers.executeUiSafeObservable())
.subscribe(r -> {
Optional<IkeyTrust> trustOptional = ikeyRepository.loadSuperordinateTrust(
getAccountId().getValue(),
JidCreate.entityBareFromOrThrowUnchecked(getContactAccountAddress().getValue()),
new OpenPgpV4Fingerprint(r.getSuperordinate())
).blockingFirst();
contactIkeyFingerprint.postValue(
new Optional<>(new FingerprintViewItem(
getAccountId().getValue(),
JidCreate.entityBareFromOrThrowUnchecked(getContactAccountAddress().getValue()),
new OpenPgpV4Fingerprint(r.getSuperordinate()),
r.getTimestamp(),
new Date(),
trustOptional.isPresent() ? trustOptional.getItem().getTrust() : OpenPgpTrustStore.Trust.undecided)));
}));
}
public LiveData<String> getContactAddress() {
return contactAddress;
}
@Override
protected void onCleared() {
super.onCleared();
disposable.dispose();
if (roster != null) {
roster.removePresenceEventListener(presenceEventListener);
}
}
public LiveData<UUID> getAccountId() {
return contactAccountId;
}
public LiveData<Drawable> getContactAvatar() {
return contactAvatar;
}
public LiveData<Presence.Mode> getContactPresenceMode() {
return contactPresenceMode;
}
public LiveData<String> getContactName() {
return contactName;
}
public LiveData<String> getContactPresenceStatus() {
return contactPresenceStatus;
}
public LiveData<String> getContactAccountAddress() {
return contactAccountAddress;
}
public LiveData<List<String>> getContactGroups() {
return contactGroups;
}
public Single<UUID> getOrCreateChat() {
return peerRepository.getPeer(peerId)
.flatMapSingle(directChatRepository::getOrCreateChatWithPeer)
.map(Chat::getId);
}
private final PresenceEventListener presenceEventListener = new CombinedPresenceListener() {
@Override
public void presenceReceived(Jid address, Presence presence) {
if (presence.getFrom().asBareJid().toString().equals(getContactAddress().getValue())) {
contactPresenceMode.postValue(presence.getMode());
contactPresenceStatus.postValue(presence.getStatus());
}
}
};
public void changeContactName(String newName)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
if (!newName.trim().isEmpty()) {
RosterEntry entry = roster.getEntry(JidCreate.entityBareFromOrThrowUnchecked(getContactAddress().getValue()));
entry.setName(newName);
}
}
public Completable addContactToRosterGroup() {
return Completable.fromAction(() -> doAddContactToRosterGroup());
}
private void doAddContactToRosterGroup() throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
String groupName = "Mercury Seven";
RosterGroup group = roster.getGroup(groupName);
if (group == null) {
group = roster.createGroup(groupName);
}
BareJid jid = JidCreate.entityBareFromOrThrowUnchecked(getContactAddress().getValue());
if (group.contains(jid)) {
return;
}
RosterEntry entry = roster.getEntry(jid);
group.addEntry(entry);
}
public Completable removeContactFromRosterGroup(String group) {
return Completable.fromAction(() -> roster.getGroup(group).removeEntry(roster.getEntry(JidCreate.entityBareFromOrThrowUnchecked(getContactAddress().getValue()))));
}
public LiveData<List<FingerprintViewItem>> getContactFingerprints() {
return contactFingerprints;
}
public void markFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, boolean checked) {
openPgpRepository.storeTrust(contactAccountId.getValue(),
JidCreate.entityBareFromOrThrowUnchecked(contactAddress.getValue()),
fingerprint,
checked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted)
.subscribe();
}
public LiveData<Optional<FingerprintViewItem>> getContactIkeyFingerprint() {
return contactIkeyFingerprint;
}
}

View File

@ -27,4 +27,8 @@
android:id="@+id/action_setup_ikey" android:id="@+id/action_setup_ikey"
android:title="Ikey Setup" /> android:title="Ikey Setup" />
<item
android:id="@+id/action_publish_ikey_element"
android:title="Publish Ikey Element" />
</menu> </menu>

View File

@ -4,6 +4,7 @@ import org.mercury_im.messenger.cli.MercuryCli;
import org.mercury_im.messenger.cli.di.module.CliDatabaseModule; import org.mercury_im.messenger.cli.di.module.CliDatabaseModule;
import org.mercury_im.messenger.cli.di.module.CliSchedulersModule; import org.mercury_im.messenger.cli.di.module.CliSchedulersModule;
import org.mercury_im.messenger.cli.di.module.X509WorkaroundConnectionFactoryModule; import org.mercury_im.messenger.cli.di.module.X509WorkaroundConnectionFactoryModule;
import org.mercury_im.messenger.core.di.module.IkeyModule;
import org.mercury_im.messenger.core.di.module.OpenPgpModule; 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.RxMercuryMessageStoreFactoryModule;
import org.mercury_im.messenger.core.di.module.RxMercuryRosterStoreFactoryModule; import org.mercury_im.messenger.core.di.module.RxMercuryRosterStoreFactoryModule;
@ -26,7 +27,8 @@ import dagger.Component;
RxMercuryMessageStoreFactoryModule.class, RxMercuryMessageStoreFactoryModule.class,
RxMercuryRosterStoreFactoryModule.class, RxMercuryRosterStoreFactoryModule.class,
OpenPgpModule.class, OpenPgpModule.class,
StanzaIdSourceFactoryModule.class StanzaIdSourceFactoryModule.class,
IkeyModule.class
}) })
public interface CliComponent { public interface CliComponent {

View File

@ -163,11 +163,19 @@ public final class IkeyManager extends Manager {
return new IkeyElement(mechanism.getType(), superordinateElement, signedElement, proofElement); return new IkeyElement(mechanism.getType(), superordinateElement, signedElement, proofElement);
} }
public void publishIkeyElement(IkeyElement ikeyElement) public void storeAndPublishElement(IkeyElement ikeyElement)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException { SmackException.NotConnectedException, SmackException.NoResponseException, IOException, PGPException {
store.storeIkeyRecord(connection().getUser().asEntityBareJid(), elementToRecord(ikeyElement));
publishIkeyElement(ikeyElement);
}
public void publishIkeyElement(IkeyElement element)
throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException,
SmackException.NoResponseException {
PepManager.getInstanceFor(connection()) PepManager.getInstanceFor(connection())
.publish(IkeyConstants.SUBORDINATES_NODE, new PayloadItem<>(ikeyElement)); .publish(IkeyConstants.SUBORDINATES_NODE, new PayloadItem<>(element));
} }
public IkeyElement fetchIkeyElementOf(EntityBareJid jid) public IkeyElement fetchIkeyElementOf(EntityBareJid jid)
@ -201,7 +209,8 @@ public final class IkeyManager extends Manager {
throws IOException, UnsupportedSignatureAlgorithmException, PGPException { throws IOException, UnsupportedSignatureAlgorithmException, PGPException {
IkeyRecord newRecord = elementToRecord(element); IkeyRecord newRecord = elementToRecord(element);
if (isFromTheFuture(newRecord)) { if (isFromTheFuture(newRecord)) {
LOGGER.log(Level.WARNING, "Received ikey element appears to be from the future: " + element.getSignedElement().getChildElement().getTimestamp()); LOGGER.log(Level.WARNING, "Received ikey element appears to be from the future: " +
newRecord.getTimestamp());
return; return;
} }
@ -307,9 +316,9 @@ public final class IkeyManager extends Manager {
} }
private static boolean isFromTheFuture(IkeyRecord record) { private static boolean isFromTheFuture(IkeyRecord record) {
Date elementTimestamp = record.getTimestamp(); Date timestamp = record.getTimestamp();
Date now = new Date(); Date now = new Date();
return elementTimestamp.after(now); return timestamp.after(now);
} }
private boolean existsSameOrNewerRecord(IkeyRecord record) throws IOException { private boolean existsSameOrNewerRecord(IkeyRecord record) throws IOException {

View File

@ -0,0 +1,9 @@
package org.mercury_im.messenger.core.util;
import lombok.Data;
@Data
public class Tuple<A, B> {
private final A first;
private final B second;
}

View File

@ -1,6 +1,5 @@
package org.mercury_im.messenger.core.viewmodel.account.detail; package org.mercury_im.messenger.core.viewmodel.account.detail;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smackx.ikey.IkeyManager; import org.jivesoftware.smackx.ikey.IkeyManager;
@ -88,7 +87,7 @@ public class AccountDetailsViewModel implements MercuryViewModel {
IkeyElement ikeyElement = syncCreateIkeyElement(accountId); IkeyElement ikeyElement = syncCreateIkeyElement(accountId);
IkeyManager ikeyManager = IkeyManager.getInstanceFor(connectionManager.getConnection(accountId).getConnection()); IkeyManager ikeyManager = IkeyManager.getInstanceFor(connectionManager.getConnection(accountId).getConnection());
ikeyManager.publishIkeyElement(ikeyElement); ikeyManager.storeAndPublishElement(ikeyElement);
}) })
.compose(schedulers.executeUiSafeCompletable()) .compose(schedulers.executeUiSafeCompletable())
.subscribe( .subscribe(
@ -96,7 +95,7 @@ public class AccountDetailsViewModel implements MercuryViewModel {
e -> LOGGER.log(Level.SEVERE, "Error publishing ikey element:", e))); e -> LOGGER.log(Level.SEVERE, "Error publishing ikey element:", e)));
} }
private IkeyElement syncCreateIkeyElement(UUID accountId) throws URISyntaxException, PGPException, IOException { private IkeyElement syncCreateIkeyElement(UUID accountId) throws URISyntaxException, IOException {
MercuryConnection connection = connectionManager.getConnection(accountId); MercuryConnection connection = connectionManager.getConnection(accountId);
IkeyManager ikeyManager = ikeyInitializer.initFor(connection); IkeyManager ikeyManager = ikeyInitializer.initFor(connection);
@ -124,8 +123,9 @@ public class AccountDetailsViewModel implements MercuryViewModel {
subordinateElements.add(new SubordinateElement("urn:xmpp:openpgp:0", u, fp.toString())); subordinateElements.add(new SubordinateElement("urn:xmpp:openpgp:0", u, fp.toString()));
} }
PGPSecretKeyRing secretKeys = openPgpRepository.loadSecretKeysOf(accountId, account.getJid()) PGPSecretKeyRing secretKeys = ikeyRepository.loadSecretKey(accountId)
.blockingGet().getSecretKeyRing(localFp.getKeyId()); .blockingFirst(new Optional<>())
.getItem();
IkeyElement ikeyElement = ikeyManager.createOxIkeyElement(secretKeys, IkeyElement ikeyElement = ikeyManager.createOxIkeyElement(secretKeys,
new UnprotectedKeysProtector(), subordinateElements.toArray(new SubordinateElement[]{})); new UnprotectedKeysProtector(), subordinateElements.toArray(new SubordinateElement[]{}));

View File

@ -0,0 +1,252 @@
package org.mercury_im.messenger.core.viewmodel.contact;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterGroup;
import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.DirectChatRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
import org.mercury_im.messenger.core.util.CombinedPresenceListener;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.util.Tuple;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.chat.Chat;
import org.mercury_im.messenger.entity.contact.Peer;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.subjects.BehaviorSubject;
import lombok.Getter;
public class ContactDetailViewModel implements MercuryViewModel {
private final MercuryConnectionManager connectionManager;
private final PeerRepository peerRepository;
private final DirectChatRepository directChatRepository;
private final OpenPgpRepository openPgpRepository;
private final IkeyRepository ikeyRepository;
private final SchedulersFacade schedulers;
private Roster roster;
private FilteredPresenceEventListener presenceEventListener;
private BehaviorSubject<EntityBareJid> contactAddress = BehaviorSubject.create();
private BehaviorSubject<String> contactDisplayName = BehaviorSubject.create();
private BehaviorSubject<EntityBareJid> contactAccountAddress = BehaviorSubject.create();
private BehaviorSubject<Presence.Mode> contactPresenceMode = BehaviorSubject.create();
private BehaviorSubject<String> contactPresenceStatus = BehaviorSubject.create();
private BehaviorSubject<List<String>> contactGroups = BehaviorSubject.create();
private BehaviorSubject<Optional<FingerprintViewItem>> contactIdentityFingerprint = BehaviorSubject.createDefault(new Optional<>());
private BehaviorSubject<List<FingerprintViewItem>> contactDeviceFingerprints = BehaviorSubject.createDefault(Collections.emptyList());
private BehaviorSubject<Tuple<String, EntityBareJid>> contactAvatarBase = BehaviorSubject.create();
@Getter
private UUID peerId;
@Getter
private UUID accountId;
@Inject
public ContactDetailViewModel(MercuryConnectionManager connectionManager,
PeerRepository peerRepository,
DirectChatRepository directChatRepository,
OpenPgpRepository openPgpRepository,
IkeyRepository ikeyRepository,
SchedulersFacade schedulers) {
this.connectionManager = connectionManager;
this.peerRepository = peerRepository;
this.directChatRepository = directChatRepository;
this.openPgpRepository = openPgpRepository;
this.ikeyRepository = ikeyRepository;
this.schedulers = schedulers;
}
public Completable init(UUID peerId) {
return peerRepository.getPeer(peerId)
.flatMapCompletable(this::init);
}
public Completable init(Peer peer) {
return Completable.fromAction(() -> {
this.peerId = peer.getId();
this.accountId = peer.getAccount().getId();
MercuryConnection connection = connectionManager.getConnection(peer.getAccount());
roster = Roster.getInstanceFor(connection.getConnection());
setupPresenceEventListener(roster, peer.getJid());
addDisposable(peerRepository
.observePeer(peerId)
.filter(Optional::isPresent)
.map(Optional::getItem)
.compose(schedulers.executeUiSafeObservable())
.subscribe(p -> {
contactAddress.onNext(peer.getJid());
contactDisplayName.onNext(peer.getDisplayName());
contactAccountAddress.onNext(peer.getAccount().getJid());
contactAvatarBase.onNext(new Tuple<>(peer.getDisplayName(), peer.getJid()));
}));
addDisposable(openPgpRepository
.observeFingerprints(peer.getAccount().getId(), peer.getJid())
.compose(schedulers.executeUiSafeObservable())
.subscribe(fingerprints -> contactDeviceFingerprints.onNext(fingerprints)));
addDisposable(ikeyRepository
.loadRecord(peer.getAccount().getId(), peer.getJid())
.compose(schedulers.executeUiSafeObservable())
.map(record -> new FingerprintViewItem(
peer.getAccount().getId(), record.getJid(),
new OpenPgpV4Fingerprint(record.getSuperordinate()),
record.getTimestamp(), record.getTimestamp(),
OpenPgpTrustStore.Trust.trusted) // TODO
)
.map(Optional::new)
.subscribe(contactIdentityFingerprint::onNext));
});
}
private void setupPresenceEventListener(Roster roster, EntityBareJid jid) {
if (presenceEventListener != null) {
if (presenceEventListener.getJid().equals(jid)) {
// already set up
return;
}
// outdated listener from previous contact
roster.removePresenceEventListener(presenceEventListener);
}
presenceEventListener = new FilteredPresenceEventListener(jid) {
@Override
public void presenceReceived(Presence presence) {
contactPresenceMode.onNext(presence.getMode());
if (presence.getStatus() != null) {
contactPresenceStatus.onNext(presence.getStatus());
}
RosterEntry entry = roster.getEntry(jid);
if (entry == null) {
return;
}
List<RosterGroup> groups = entry.getGroups();
List<String> groupNames = new ArrayList<>(groups.size());
for (RosterGroup group : groups) {
groupNames.add(group.getName());
}
contactGroups.onNext(groupNames);
}
};
// Trigger once
Presence presence = roster.getPresence(jid);
if (presence != null) {
presenceEventListener.presenceReceived(presence);
}
roster.addPresenceEventListener(presenceEventListener);
}
public Observable<EntityBareJid> getContactAddress() {
return contactAddress;
}
public Observable<String> getContactDisplayName() {
return contactDisplayName;
}
public Observable<EntityBareJid> getContactAccountAddress() {
return contactAccountAddress;
}
public Observable<Presence.Mode> getContactPresenceMode() {
return contactPresenceMode;
}
public Observable<String> getContactPresenceStatus() {
return contactPresenceStatus;
}
public Observable<List<String>> getContactGroups() {
return contactGroups;
}
public Observable<Optional<FingerprintViewItem>> getContactIdentityFingerprint() {
return contactIdentityFingerprint;
}
public Observable<List<FingerprintViewItem>> getContactDeviceFingerprints() {
return contactDeviceFingerprints;
}
public Observable<Tuple<String, EntityBareJid>> getContactAvatarBase() {
return contactAvatarBase;
}
public Single<UUID> getOrCreateChat() {
return peerRepository.getPeer(peerId)
.flatMapSingle(directChatRepository::getOrCreateChatWithPeer)
.map(Chat::getId);
}
public void changeContactName(String newName)
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException,
InterruptedException, SmackException.NoResponseException {
if (!newName.trim().isEmpty()) {
RosterEntry entry = roster.getEntry(contactAddress.getValue());
entry.setName(newName);
}
}
public void markDeviceFingerprintTrusted(OpenPgpV4Fingerprint fingerprint, boolean checked) {
openPgpRepository.storeTrust(accountId, contactAddress.getValue(), fingerprint,
checked ? OpenPgpTrustStore.Trust.trusted : OpenPgpTrustStore.Trust.untrusted)
.subscribe();
}
public static abstract class FilteredPresenceEventListener extends CombinedPresenceListener {
@Getter
private final EntityBareJid jid;
public FilteredPresenceEventListener(EntityBareJid jid) {
this.jid = jid;
}
@Override
public void presenceReceived(Jid address, Presence presence) {
EntityBareJid entityBareJid = address.asEntityBareJidIfPossible();
if (entityBareJid != null && entityBareJid.equals(jid)) {
presenceReceived(presence);
}
}
public abstract void presenceReceived(Presence presence);
}
@Override
public void dispose() {
compositeDisposable.dispose();
if (presenceEventListener != null) roster.removePresenceEventListener(presenceEventListener);
}
}