Wip: Implement deleting account and displaying fingerprints

This commit is contained in:
Paul Schaub 2020-07-04 01:17:18 +02:00
parent 4bab6244b3
commit 940a223563
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
17 changed files with 309 additions and 178 deletions

View File

@ -15,6 +15,8 @@ import com.google.android.material.navigation.NavigationView;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.ui.account.DeleteAccountDialogFragment;
import org.mercury_im.messenger.android.ui.account.OnAccountListItemClickListener;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.android.ui.chatlist.ChatListFragment;
@ -29,7 +31,7 @@ import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener,
AccountsFragment.OnAccountListItemClickListener {
OnAccountListItemClickListener {
@BindView(R.id.toolbar)
Toolbar toolbar;
@ -104,6 +106,6 @@ public class MainActivity extends AppCompatActivity
@Override
public void onAccountListItemLongClick(Account item) {
accountRepository.deleteAccount(item).subscribe();
new DeleteAccountDialogFragment(item.getId()).show(getSupportFragmentManager(), "DELETE");
}
}

View File

@ -1,10 +0,0 @@
package org.mercury_im.messenger.android.ui.account;
public class AccountViewItem {
private boolean enabled;
private String jid;
private String status;
private String fingerprint;
}

View File

@ -62,8 +62,8 @@ public class AccountsFragment extends Fragment {
}
private void observeViewModel() {
viewModel.getConnectionPool().observe(this, pool ->
adapter.setValues(new ArrayList<>(pool.getConnectionStates().values())));
viewModel.getAccounts().observe(this,
accounts -> adapter.setValues(new ArrayList<>(accounts)));
}
@Override
@ -97,22 +97,6 @@ public class AccountsFragment extends Fragment {
accountClickListener = null;
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p/>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnAccountListItemClickListener {
void onAccountListItemClick(Account item);
void onAccountListItemLongClick(Account item);
}
private void displayAddAccountDialog() {
Logger.getAnonymousLogger().log(Level.INFO, "DISPLAY FRAGMENT!!!");
AddAccountDialogFragment addAccountDialogFragment = new AddAccountDialogFragment();

View File

@ -12,19 +12,18 @@ import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.util.OpenPgpFingerprintColorizer;
import org.mercury_im.messenger.core.viewmodel.accounts.AccountViewItem;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.android.ui.avatar.AvatarDrawable;
import org.mercury_im.messenger.android.ui.account.AccountsFragment.OnAccountListItemClickListener;
import org.mercury_im.messenger.android.util.AbstractDiffCallback;
import org.mercury_im.messenger.core.xmpp.MercuryConnection;
import org.mercury_im.messenger.core.xmpp.state.ConnectionState;
import java.util.ArrayList;
import java.util.List;
public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRecyclerViewAdapter.ViewHolder> {
private final List<ConnectionState> connectionStates = new ArrayList<>();
private final List<AccountViewItem> connectionStates = new ArrayList<>();
private final OnAccountListItemClickListener onAccountClickListener;
private final AndroidAccountsViewModel viewModel;
@ -38,10 +37,11 @@ public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRe
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_account, parent, false);
return new ViewHolder(view);
}
public void setValues(List<ConnectionState> values) {
public void setValues(List<AccountViewItem> values) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new AccountsDiffCallback(values, connectionStates), true);
connectionStates.clear();
@ -56,26 +56,34 @@ public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRe
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
ConnectionState state = connectionStates.get(position);
MercuryConnection connection = state.getConnection();
Account account = connection.getAccount();
AccountViewItem viewItem = connectionStates.get(position);
Account account = viewItem.getAccount();
holder.account = account;
holder.jid.setText(account.getAddress());
holder.avatar.setImageDrawable(new AvatarDrawable(account.getAddress(), account.getAddress()));
holder.enabled.setChecked(account.isEnabled());
holder.enabled.setOnCheckedChangeListener((compoundButton, checked) ->
viewModel.setAccountEnabled(account, checked));
holder.status.setText(state.getConnectivity().toString());
holder.status.setText(viewItem.getConnectivityState().toString());
if (viewItem.getFingerprint() != null) {
holder.fingerprint.setText(OpenPgpFingerprintColorizer.formatOpenPgpV4Fingerprint(viewItem.getFingerprint()));
}
holder.mView.setOnLongClickListener(v -> {
onAccountClickListener.onAccountListItemLongClick(account);
return true;
});
holder.mView.setOnClickListener(v -> onAccountClickListener.onAccountListItemClick(account));
}
class ViewHolder extends RecyclerView.ViewHolder {
private Account account;
final View mView;
final ImageView avatar;
final TextView jid;
final Switch enabled;
final TextView status;
final TextView fingerprint;
public ViewHolder(View view) {
super(view);
@ -84,25 +92,26 @@ public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRe
jid = view.findViewById(R.id.text_account_jid);
enabled = view.findViewById(R.id.switch_account_enabled);
status = view.findViewById(R.id.text_account_status);
fingerprint = view.findViewById(R.id.fingerprint);
}
}
public class AccountsDiffCallback extends AbstractDiffCallback<ConnectionState> {
public class AccountsDiffCallback extends AbstractDiffCallback<AccountViewItem> {
public AccountsDiffCallback(List<ConnectionState> newItems, List<ConnectionState> oldItems) {
public AccountsDiffCallback(List<AccountViewItem> newItems, List<AccountViewItem> oldItems) {
super(newItems, oldItems);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).getConnection().getAccount().getId()
.equals(newItems.get(newItemPosition).getConnection().getAccount().getId());
return oldItems.get(oldItemPosition).getAccount().getId()
.equals(newItems.get(newItemPosition).getAccount().getId());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
ConnectionState oldM = oldItems.get(oldItemPosition);
ConnectionState newM = newItems.get(newItemPosition);
AccountViewItem oldM = oldItems.get(oldItemPosition);
AccountViewItem newM = newItems.get(newItemPosition);
return oldM.equals(newM);
}
}

View File

@ -8,11 +8,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.core.viewmodel.accounts.AccountViewItem;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.viewmodel.accounts.AccountsViewModel;
import org.mercury_im.messenger.core.xmpp.state.ConnectionPoolState;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -22,7 +25,7 @@ public class AndroidAccountsViewModel extends AndroidViewModel implements Mercur
private static final Logger LOGGER = Logger.getLogger(AndroidAccountsViewModel.class.getName());
private final MutableLiveData<ConnectionPoolState> connectionPool = new MutableLiveData<>();
private final MutableLiveData<List<AccountViewItem>> accounts = new MutableLiveData<>(Collections.emptyList());
@Inject
AccountsViewModel viewModel;
@ -32,13 +35,12 @@ public class AndroidAccountsViewModel extends AndroidViewModel implements Mercur
MercuryImApplication.getApplication().getAppComponent().inject(this);
addDisposable(getCommonViewModel().getConnectionPool()
.subscribe(connectionPool::postValue,
error -> LOGGER.log(Level.SEVERE, "Error observing connections", error)));
addDisposable(getCommonViewModel().observeAccounts()
.subscribe(accounts::postValue, error -> LOGGER.log(Level.SEVERE, "Error observing accounts", error)));
}
public LiveData<ConnectionPoolState> getConnectionPool() {
return connectionPool;
public LiveData<List<AccountViewItem>> getAccounts() {
return accounts;
}
public void setAccountEnabled(Account accountModel, boolean enabled) {
@ -49,4 +51,8 @@ public class AndroidAccountsViewModel extends AndroidViewModel implements Mercur
public AccountsViewModel getCommonViewModel() {
return viewModel;
}
public void onDeleteAccount(UUID accountId) {
getCommonViewModel().onDeleteAccount(accountId);
}
}

View File

@ -6,26 +6,21 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.android.util.TextChangedListener;
import org.mercury_im.messenger.core.account.error.PasswordError;
import org.mercury_im.messenger.core.account.error.UsernameError;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.android.util.TextChangedListener;
import org.mercury_im.messenger.core.viewmodel.accounts.LoginViewModel;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAndroidViewModel<LoginViewModel> {
private static final Logger LOGGER = Logger.getLogger(AndroidLoginViewModel.class.getName());
private final MutableLiveData<String> loginUsernameError = new MutableLiveData<>();
private final MutableLiveData<String> loginPasswordError = new MutableLiveData<>();
private final MutableLiveData<Boolean> loginButtonEnabled = new MutableLiveData<>(true);
@ -43,9 +38,11 @@ public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAn
public void reset() {
MercuryImApplication.getApplication().getAppComponent().inject(this);
commonViewModel.reset();
addDisposable(mapUsernameErrorCodeToMessage(getCommonViewModel().getLoginUsernameError())
addDisposable(getCommonViewModel().getLoginUsernameError()
.compose(usernameErrorToErrorMessage)
.subscribe(e -> loginUsernameError.setValue(e.isPresent() ? e.getItem() : null)));
addDisposable(mapPasswordErrorCodeToMessage(getCommonViewModel().getLoginPasswordError())
addDisposable(getCommonViewModel().getLoginPasswordError()
.compose(passwordErrorToErrorMessage)
.subscribe(e -> loginPasswordError.setValue(e.isPresent() ? e.getItem() : null)));
addDisposable(getCommonViewModel().isLoginPossible()
.subscribe(loginButtonEnabled::setValue));
@ -53,50 +50,48 @@ public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAn
.subscribe(loginFinished::setValue));
}
private Observable<Optional<String>> mapUsernameErrorCodeToMessage(Observable<Optional<UsernameError>> errorCodeObservable) {
return errorCodeObservable.map(optional -> {
if (!optional.isPresent()){
return new Optional<>(null);
}
UsernameError error = optional.getItem();
int resourceId;
switch (error) {
case emptyUsername:
resourceId = R.string.error_field_required;
break;
case invalidUsername:
resourceId = R.string.error_invalid_username;
break;
case unreachableServer:
resourceId = R.string.error_unreachable_server;
break;
default:
resourceId = R.string.error_uknown_error;
}
return new Optional<>(getApplication().getResources().getString(resourceId));
});
}
private final ObservableTransformer<Optional<UsernameError>, Optional<String>> usernameErrorToErrorMessage =
upstream -> upstream.map(optional -> {
if (!optional.isPresent()){
return new Optional<>(null);
}
UsernameError error = optional.getItem();
int resourceId;
switch (error) {
case emptyUsername:
resourceId = R.string.error_field_required;
break;
case invalidUsername:
resourceId = R.string.error_invalid_username;
break;
case unreachableServer:
resourceId = R.string.error_unreachable_server;
break;
default:
resourceId = R.string.error_uknown_error;
}
return new Optional<>(getApplication().getResources().getString(resourceId));
});
private Observable<Optional<String>> mapPasswordErrorCodeToMessage(Observable<Optional<PasswordError>> errorCodeObservable) {
return errorCodeObservable.map(optional -> {
if (!optional.isPresent()){
return new Optional<>(null);
}
PasswordError error = optional.getItem();
int resourceId;
switch (error) {
case emptyPassword:
resourceId = R.string.error_field_required;
break;
case incorrectPassword:
resourceId = R.string.error_incorrect_password;
break;
default:
resourceId = R.string.error_uknown_error;
}
return new Optional<>(getApplication().getResources().getString(resourceId));
});
}
private final ObservableTransformer<Optional<PasswordError>, Optional<String>> passwordErrorToErrorMessage =
upstream -> upstream.map(optional -> {
if (!optional.isPresent()) {
return new Optional<>(null);
}
PasswordError error = optional.getItem();
int resourceId;
switch (error) {
case emptyPassword:
resourceId = R.string.error_field_required;
break;
case incorrectPassword:
resourceId = R.string.error_incorrect_password;
break;
default:
resourceId = R.string.error_uknown_error;
}
return new Optional<>(getApplication().getResources().getString(resourceId));
});
@Override
protected void onCleared() {

View File

@ -0,0 +1,34 @@
package org.mercury_im.messenger.android.ui.account;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.R;
import java.util.UUID;
public class DeleteAccountDialogFragment extends AppCompatDialogFragment {
private AndroidAccountsViewModel viewModel;
private final UUID accountId;
public DeleteAccountDialogFragment(UUID accountId) {
this.accountId = accountId;
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
viewModel = new ViewModelProvider(requireActivity()).get(AndroidAccountsViewModel.class);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Do you really want to delete this account?")
.setPositiveButton(R.string.button_delete, (dialog, id) -> viewModel.onDeleteAccount(accountId))
.setNegativeButton(R.string.button_cancel, (dialog, id) -> dialog.dismiss());
return builder.create();
}
}

View File

@ -0,0 +1,19 @@
package org.mercury_im.messenger.android.ui.account;
import org.mercury_im.messenger.entity.Account;
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p/>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnAccountListItemClickListener {
void onAccountListItemClick(Account item);
void onAccountListItemLongClick(Account item);
}

View File

@ -22,6 +22,7 @@ import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import org.jivesoftware.smack.util.StringUtils;
import org.mercury_im.messenger.android.util.ColorUtil;
/**
@ -45,7 +46,17 @@ public class AvatarDrawable extends ColorDrawable {
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setAntiAlias(true);
textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL));
this.letter = name == null ? null : String.valueOf(Character.toUpperCase(name.charAt(0)));
this.letter = StringUtils.isNullOrEmpty(name) ? "?" : getInitials(name).toUpperCase();
}
private static String getInitials(String name) {
String trimmedName = name.trim();
String[] words = trimmedName.split(" ");
if (words.length == 1) {
return words[0].charAt(0) + (words[0].length() == 1 ? "" : "" + words[0].charAt(1));
} else {
return words[0].charAt(0) + "" + words[words.length - 1].charAt(0);
}
}
@Override

View File

@ -8,6 +8,7 @@ import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.Messenger;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.data.repository.DirectChatRepository;
import org.mercury_im.messenger.core.data.repository.MessageRepository;
import org.mercury_im.messenger.core.data.repository.PeerRepository;
@ -47,6 +48,9 @@ public class AndroidChatViewModel extends ViewModel implements MercuryAndroidVie
@Inject
MessageRepository messageRepository;
@Inject
SchedulersFacade schedulers;
@Inject
Messenger messenger;
@ -121,8 +125,8 @@ public class AndroidChatViewModel extends ViewModel implements MercuryAndroidVie
public void deleteContact() {
Peer contact = getContact().getValue();
disposable.add(messenger.deleteContact(contact)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(schedulers.getIoScheduler())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "Contact deleted."), e -> {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}));
@ -130,7 +134,9 @@ public class AndroidChatViewModel extends ViewModel implements MercuryAndroidVie
public void sendMessage(String body) {
disposable.add(messenger.sendMessage(getContact().getValue(), body)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe());
.subscribeOn(schedulers.getIoScheduler())
.observeOn(schedulers.getUiScheduler())
.subscribe());
}
@Override

View File

@ -7,6 +7,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout_jid"
android:layout_width="match_parent"
@ -26,24 +27,6 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_layout_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp">
<EditText
android:id="@+id/input_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_password"
tools:text="swordfish"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -1,57 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="66dp">
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar_account"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_account_circle_black_24dp"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="66dp">
<ImageView
android:id="@+id/avatar_account"
android:contentDescription="@string/content_description_avatar_image"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_account_circle_black_24dp"/>
<TextView
android:id="@+id/text_account_jid"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:singleLine="true"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="8dp"
android:autoSizeMaxTextSize="18dp"
app:layout_constraintEnd_toStartOf="@+id/switch_account_enabled"
app:layout_constraintStart_toEndOf="@+id/avatar_account"
app:layout_constraintTop_toTopOf="parent"
tools:text="buzzlightyear@jabber.nasa.gov" />
<TextView
android:id="@+id/text_account_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@+id/avatar_account"
app:layout_constraintTop_toBottomOf="@+id/text_account_jid"
tools:text="Online" />
<Switch
android:id="@+id/switch_account_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_account_jid"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:singleLine="true"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="8dp"
android:autoSizeMaxTextSize="18dp"
app:layout_constraintEnd_toStartOf="@+id/switch_account_enabled"
app:layout_constraintStart_toEndOf="@+id/avatar_account"
app:layout_constraintTop_toTopOf="parent"
tools:text="buzzlightyear@jabber.nasa.gov" />
android:layout_gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:text="Fingerprint" />
<TextView
android:id="@+id/text_account_status"
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@+id/avatar_account"
app:layout_constraintTop_toBottomOf="@+id/text_account_jid"
tools:text="Online" />
android:layout_gravity="center"
android:textSize="20dp"
android:typeface="monospace"
tools:text="1357 B018 65B2 503C 1845\n3D20 8CAC 2A96 7854 8E35" />
<Switch
android:id="@+id/switch_account_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -140,4 +140,6 @@
<string name="error_invalid_address">Invalid address</string>
<string name="error_unreachable_server">Server unreachable</string>
<string name="error_uknown_error">Unknown error</string>
<string name="content_description_avatar_image">Avatar Image</string>
<string name="button_delete">Delete</string>
</resources>

View File

@ -2,6 +2,7 @@ package org.mercury_im.messenger.core.di.module;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.data.repository.Repositories;
import org.mercury_im.messenger.core.viewmodel.accounts.AccountsViewModel;
import org.mercury_im.messenger.core.viewmodel.accounts.LoginViewModel;
@ -30,8 +31,9 @@ public class ViewModelModule {
@Singleton
static AccountsViewModel provideAccountsViewModel(MercuryConnectionManager connectionManager,
AccountRepository accountRepository,
OpenPgpRepository openPgpRepository,
SchedulersFacade schedulers) {
return new AccountsViewModel(connectionManager, accountRepository, schedulers);
return new AccountsViewModel(connectionManager, accountRepository, openPgpRepository, schedulers);
}
/*

View File

@ -0,0 +1,16 @@
package org.mercury_im.messenger.core.viewmodel.accounts;
import org.mercury_im.messenger.core.xmpp.state.ConnectivityState;
import org.mercury_im.messenger.entity.Account;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import lombok.Value;
@Value
public class AccountViewItem {
Account account;
ConnectivityState connectivityState;
OpenPgpV4Fingerprint fingerprint;
}

View File

@ -1,16 +1,29 @@
package org.mercury_im.messenger.core.viewmodel.accounts;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.data.repository.OpenPgpRepository;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.core.xmpp.MercuryConnectionManager;
import org.mercury_im.messenger.core.xmpp.state.ConnectionPoolState;
import org.mercury_im.messenger.core.xmpp.state.ConnectionState;
import org.mercury_im.messenger.entity.Account;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.ObservableTransformer;
public class AccountsViewModel implements MercuryViewModel {
@ -18,20 +31,24 @@ public class AccountsViewModel implements MercuryViewModel {
private final MercuryConnectionManager connectionManager;
private final AccountRepository accountRepository;
private final OpenPgpRepository openPgpRepository;
private final SchedulersFacade schedulers;
@Inject
public AccountsViewModel(MercuryConnectionManager connectionManager,
AccountRepository accountRepository,
OpenPgpRepository openPgpRepository,
SchedulersFacade schedulers) {
this.connectionManager = connectionManager;
this.accountRepository = accountRepository;
this.openPgpRepository = openPgpRepository;
this.schedulers = schedulers;
}
public Observable<ConnectionPoolState> getConnectionPool() {
return connectionManager.observeConnectionPool();
public Observable<List<AccountViewItem>> observeAccounts() {
return connectionManager.observeConnectionPool()
.compose(transformer);
}
public void onToggleAccountEnabled(Account account, boolean enabled) {
@ -53,4 +70,32 @@ public class AccountsViewModel implements MercuryViewModel {
private void logAccountToggleError(Account account, boolean enabled, Throwable error) {
LOGGER.log(Level.SEVERE, "Account " + account.getAddress() + " could not be " + (enabled ? "enabled" : "disabled"), error);
}
private final ObservableTransformer<ConnectionPoolState, List<AccountViewItem>> transformer = new ObservableTransformer<ConnectionPoolState, List<AccountViewItem>>() {
@Override
public ObservableSource<List<AccountViewItem>> apply(Observable<ConnectionPoolState> upstream) {
return upstream.map(state -> {
List<AccountViewItem> viewItems = new ArrayList<>();
for (Map.Entry<UUID, ConnectionState> entry : state.getConnectionStates().entrySet()) {
ConnectionState connectionState = entry.getValue();
Account account = accountRepository.getAccount(entry.getKey()).blockingGet();
OpenPgpV4Fingerprint fingerprint = null;
try {
PGPSecretKeyRingCollection secretKeyRings = openPgpRepository.loadSecretKeysOf(account.getId(), account.getJid()).blockingGet();
fingerprint = new OpenPgpV4Fingerprint(secretKeyRings.getKeyRings().next());
} catch (NoSuchElementException e) {
LOGGER.log(Level.INFO, "No fingerprint found for " + account.getAddress());
}
viewItems.add(new AccountViewItem(account, connectionState.getConnectivity(), fingerprint));
}
return viewItems;
});
}
};
public void onDeleteAccount(UUID accountId) {
addDisposable(accountRepository.deleteAccount(accountId)
.subscribe(() -> LOGGER.log(Level.INFO, "Account " + accountId + " successfully deleted."),
e -> LOGGER.log(Level.SEVERE, "Could not delete account " + accountId, e)));
}
}

View File

@ -18,6 +18,9 @@ public class Message {
String legacyStanzaId;
String originId;
String stanzaId;
boolean encrypted;
boolean received;
boolean read;
public boolean isIncoming() {
return getDirection() == MessageDirection.incoming;