Prototype account details

This commit is contained in:
Paul Schaub 2020-07-09 02:10:47 +02:00
parent 669aa061ab
commit b0ee219721
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
34 changed files with 724 additions and 112 deletions

View File

@ -5,7 +5,9 @@ import android.content.Intent;
import android.os.Build;
import org.jivesoftware.smack.android.AndroidSmackInitializer;
import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager;
import org.mercury_im.messenger.android.di.component.DaggerAppComponent;
import org.mercury_im.messenger.android.util.AndroidLoggingHandler;
import org.mercury_im.messenger.core.Messenger;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.android.di.component.AppComponent;
@ -43,11 +45,13 @@ public class MercuryImApplication extends Application {
public void onCreate() {
super.onCreate();
AndroidSmackInitializer.initialize(getApplicationContext());
AndroidLoggingHandler.reset(new AndroidLoggingHandler());
INSTANCE = this;
appComponent = createAppComponent();
appComponent.inject(this);
setupClientStateIndication();
ServerPingWithAlarmManager.onCreate(this);
Notifications.initializeNotificationChannels(this);

View File

@ -3,6 +3,7 @@ package org.mercury_im.messenger.android.di.component;
import org.mercury_im.messenger.android.MercuryImApplication;
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.ox.AndroidOxSecretKeyBackupRestoreViewModel;
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.XmppTcpConnectionFactoryModule;
@ -80,6 +81,8 @@ public interface AppComponent {
void inject(ContactDetailViewModel contactDetailViewModel);
//void inject(AndroidOxSecretKeyBackupRestoreViewModel viewModel);
// Common VMs
void inject(LoginViewModel loginViewModel);

View File

@ -30,7 +30,6 @@ public class AndroidSchedulersModule {
@Provides
@Named(value = SchedulersFacade.SCHEDULER_NEW_THREAD)
@Singleton
static Scheduler provideNewThread() {
return Schedulers.newThread();
}

View File

@ -2,7 +2,6 @@ package org.mercury_im.messenger.android.di.module;
import android.app.Application;
import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager;
import org.mercury_im.messenger.android.MercuryImApplication;
import javax.inject.Singleton;
@ -17,7 +16,6 @@ public class AppModule {
public AppModule(MercuryImApplication application) {
this.mApplication = application;
ServerPingWithAlarmManager.onCreate(application);
}
@Provides

View File

@ -15,6 +15,7 @@ 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.AccountDetailsFragment;
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;
@ -101,7 +102,7 @@ public class MainActivity extends AppCompatActivity
@Override
public void onAccountListItemClick(Account item) {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment, new AccountDetailsFragment()).commit();
}
@Override

View File

@ -0,0 +1,45 @@
package org.mercury_im.messenger.android.ui.account;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.mercury_im.messenger.R;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.hdodenhof.circleimageview.CircleImageView;
public class AccountDetailsFragment extends Fragment {
@BindView(R.id.avatar)
CircleImageView avatar;
@BindView(R.id.jid)
TextView jid;
@BindView(R.id.btn_share)
Button localFingerprintShareButton;
@BindView(R.id.fingerprint)
TextView localFingerprint;
@BindView(R.id.fingerprint_list)
ListView externalFingerprintList;
@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);
return view;
}
}

View File

@ -98,7 +98,6 @@ public class AccountsFragment extends Fragment {
}
private void displayAddAccountDialog() {
Logger.getAnonymousLogger().log(Level.INFO, "DISPLAY FRAGMENT!!!");
AddAccountDialogFragment addAccountDialogFragment = new AddAccountDialogFragment();
addAccountDialogFragment.show(getParentFragmentManager(), "addAccount");
}

View File

@ -65,9 +65,6 @@ public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRe
holder.enabled.setOnCheckedChangeListener((compoundButton, checked) ->
viewModel.setAccountEnabled(account, checked));
holder.status.setText(viewItem.getConnectivityState().toString());
if (viewItem.getFingerprint() != null) {
holder.fingerprint.setText(OpenPgpFingerprintColorizer.formatOpenPgpV4Fingerprint(viewItem.getFingerprint()));
}
holder.mView.setOnLongClickListener(v -> {
onAccountClickListener.onAccountListItemLongClick(account);
@ -83,7 +80,6 @@ public class AccountsRecyclerViewAdapter extends RecyclerView.Adapter<AccountsRe
final TextView jid;
final Switch enabled;
final TextView status;
final TextView fingerprint;
public ViewHolder(View view) {
super(view);
@ -92,7 +88,6 @@ 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);
}
}

View File

@ -8,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

View File

@ -45,7 +45,6 @@ public class ChatListRecyclerViewAdapter
@Override
public void onBindViewHolder(@NonNull ChatHolder holder, int position) {
Logger.getAnonymousLogger().log(Level.INFO, "BIND");
DirectChat model = getItemAt(position);
String name = model.getPeer().getDisplayName();
String address = model.getPeer().getAddress();

View File

@ -0,0 +1,48 @@
package org.mercury_im.messenger.android.ui.ox;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.ox.OxBackupRestoreError;
import org.mercury_im.messenger.core.viewmodel.ox.OxSecretKeyBackupRestoreViewModel;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
public class AndroidOxSecretKeyBackupRestoreViewModel extends AndroidViewModel
implements MercuryAndroidViewModel<OxSecretKeyBackupRestoreViewModel> {
private static final Logger LOGGER = Logger.getLogger(AndroidOxSecretKeyBackupRestoreViewModel.class.getName());
//@Inject
OxSecretKeyBackupRestoreViewModel commonViewModel;
private MutableLiveData<Optional<OxBackupRestoreError>> restoreError =
new MutableLiveData<>(new Optional<>());
public AndroidOxSecretKeyBackupRestoreViewModel(@NonNull Application application) {
super(application);
//MercuryImApplication.getApplication().getAppComponent().inject(this);
addDisposable(getCommonViewModel().observeBackupRestoreError()
.subscribe(opt -> restoreError.postValue(opt),
e -> LOGGER.log(Level.SEVERE, "Could not subscribe android view model to backup restore errors", e)));
}
@Override
public OxSecretKeyBackupRestoreViewModel getCommonViewModel() {
return commonViewModel;
}
public void onRestoreCodeEntered(String code) {
getCommonViewModel().onRestoreCodeEntered(code);
}
}

View File

@ -0,0 +1,48 @@
package org.mercury_im.messenger.android.ui.ox;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import org.mercury_im.messenger.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class OxSecretKeyBackupRestoreFragment extends Fragment {
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.backup_code)
EditText backupCode;
@BindView(R.id.btn_scan)
ImageButton scanButton;
@BindView(R.id.btn_cancel)
Button cancelButton;
@BindView(R.id.btn_continue)
Button continueButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ox_restore_backup, container, false);
ButterKnife.bind(this, view);
scanButton.setOnClickListener(v -> Toast.makeText(getContext(), R.string.not_yet_implemented, Toast.LENGTH_SHORT).show());
return view;
}
}

View File

@ -0,0 +1,67 @@
package org.mercury_im.messenger.android.util;
import android.util.Log;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
/**
* Make JUL work on Android.
*/
public class AndroidLoggingHandler extends Handler {
public static void reset(Handler rootHandler) {
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler[] handlers = rootLogger.getHandlers();
for (Handler handler : handlers) {
rootLogger.removeHandler(handler);
}
rootLogger.addHandler(rootHandler);
}
@Override
public void close() {
}
@Override
public void flush() {
}
@Override
public void publish(LogRecord record) {
if (!super.isLoggable(record))
return;
String name = record.getLoggerName();
int maxLength = 30;
String tag = name.length() > maxLength ? name.substring(name.length() - maxLength) : name;
try {
int level = getAndroidLevel(record.getLevel());
Log.println(level, tag, record.getMessage());
if (record.getThrown() != null) {
Log.println(level, tag, Log.getStackTraceString(record.getThrown()));
}
} catch (RuntimeException e) {
Log.e("AndroidLoggingHandler", "Error logging message.", e);
}
}
static int getAndroidLevel(Level level) {
int value = level.intValue();
if (value >= Level.SEVERE.intValue()) {
return Log.ERROR;
} else if (value >= Level.WARNING.intValue()) {
return Log.WARN;
} else if (value >= Level.INFO.intValue()) {
return Log.INFO;
} else {
return Log.DEBUG;
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@ -1,32 +0,0 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
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"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp">
<EditText
android:id="@+id/input_jid"
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_jid"
tools:text="alice@wonderland.lit" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -0,0 +1,62 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="12dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="196dp"
android:layout_height="196dp"
android:layout_marginTop="60dp"
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="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/jid">
<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" />
<include layout="@layout/fragment_fingerprint_card"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
<include layout="@layout/fragment_toggleable_fingerprints_card"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
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/fingerprint"
layout="@layout/view_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_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="share"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fingerprint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,86 @@
<?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" android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/Theme.Mercury" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="A secret key backup was found on the server. Please enter your OX backup code to restore the key on this device." />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/backup_code"
android:inputType="textNoSuggestions"
android:hint="TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toStartOf="@+id/btn_scan" />
<ImageButton
android:id="@+id/btn_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:background="@drawable/ic_qr_code_scanner_black_24dp"
android:minWidth="48dp"
android:minHeight="48dp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
app:layout_constraintTop_toBottomOf="@id/input">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_cancel" />
<Button
android:id="@+id/btn_continue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Continue" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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"
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="Other Fingerprints"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ListView
android:id="@+id/fingerprint_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:listitem="@layout/view_toggleable_fingerprint"
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -62,20 +62,4 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:text="Fingerprint" />
<TextView
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="20dp"
android:typeface="monospace"
tools:text="1357 B018 65B2 503C 1845\n3D20 8CAC 2A96 7854 8E35" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="18sp"
android:typeface="monospace"
tools:text="1357 B018 65B2 503C 1845\n3D20 8CAC 2A96 7854 8E35" />

View File

@ -0,0 +1,36 @@
<?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="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/include"
layout="@layout/view_fingerprint"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginVertical="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Switch
android:id="@+id/toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="4dp"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,7 +3,13 @@ package org.mercury_im.messenger.core;
import javax.inject.Inject;
import javax.inject.Named;
import io.reactivex.CompletableTransformer;
import io.reactivex.MaybeTransformer;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.ObservableTransformer;
import io.reactivex.Scheduler;
import io.reactivex.SingleTransformer;
import lombok.Getter;
public class SchedulersFacade {
@ -39,4 +45,20 @@ public class SchedulersFacade {
this.uiScheduler = uiScheduler;
this.newThread = newThread;
}
public <A> ObservableTransformer<A, A> executeUiSafeObservable() {
return upstream -> upstream.subscribeOn(ioScheduler).observeOn(uiScheduler);
}
public <A> SingleTransformer<A, A> executeUiSafeSingle() {
return upstream -> upstream.subscribeOn(ioScheduler).observeOn(uiScheduler);
}
public <A> MaybeTransformer<A, A> executeUiSafeMaybe() {
return upstream -> upstream.subscribeOn(ioScheduler).observeOn(uiScheduler);
}
public CompletableTransformer executeUiSafeCompletable() {
return upstream -> upstream.subscribeOn(ioScheduler).observeOn(uiScheduler);
}
}

View File

@ -1,7 +1,5 @@
package org.mercury_im.messenger.core.crypto;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smackx.ox.OpenPgpManager;
@ -19,7 +17,6 @@ import org.mercury_im.messenger.core.data.repository.Repositories;
import org.mercury_im.messenger.core.store.crypto.MercuryOpenPgpStore;
import org.mercury_im.messenger.core.store.message.MercuryMessageStore;
import org.mercury_im.messenger.core.xmpp.MercuryConnection;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.logging.Level;
import java.util.logging.Logger;

View File

@ -0,0 +1,23 @@
package org.mercury_im.messenger.core.di.component;
import org.mercury_im.messenger.core.di.scope.AccountScope;
import java.util.UUID;
import dagger.BindsInstance;
import dagger.Component;
@Component
public interface ConnectionComponent {
ConnectionComponent getComponent();
@Component.Builder
interface Builder {
@BindsInstance Builder withAccount(@AccountScope UUID accountId);
ConnectionComponent build();
}
}

View File

@ -3,18 +3,14 @@ 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;
import org.mercury_im.messenger.core.viewmodel.chat.ChatViewModel;
import org.mercury_im.messenger.core.xmpp.MercuryConnectionManager;
import javax.inject.Named;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import io.reactivex.Scheduler;
@Module
public class ViewModelModule {
@ -36,12 +32,19 @@ public class ViewModelModule {
return new AccountsViewModel(connectionManager, accountRepository, openPgpRepository, schedulers);
}
/*
@Provides
@Singleton
static OxSecretKeyBackupRestoreViewModel provideOxSecretKeyBackupRestoreViewModel(OpenPgpManager openPgpManager) {
return new OxSecretKeyBackupRestoreViewModel(openPgpManager);
}
*/
/*
@Provides
@Singleton
static ChatViewModel provideChatViewModel(Repositories repositories, SchedulersFacade schedulers) {
return new ChatViewModel(repositories, schedulers);
}
*/
}

View File

@ -0,0 +1,13 @@
package org.mercury_im.messenger.core.di.scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.inject.Scope;
@Scope
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AccountScope {
}

View File

@ -48,7 +48,7 @@ public class AccountsViewModel implements MercuryViewModel {
public Observable<List<AccountViewItem>> observeAccounts() {
return connectionManager.observeConnectionPool()
.compose(transformer);
.compose(toAccountViewItems);
}
public void onToggleAccountEnabled(Account account, boolean enabled) {
@ -64,14 +64,14 @@ public class AccountsViewModel implements MercuryViewModel {
}
private void logAccountToggledSuccess(Account account, boolean enabled) {
LOGGER.log(Level.FINER, "Account " + account.getAddress() + (enabled ? " enabled" : " disabled"));
LOGGER.log(Level.INFO, "Account " + account.getAddress() + (enabled ? " enabled" : " disabled"));
}
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>>() {
private final ObservableTransformer<ConnectionPoolState, List<AccountViewItem>> toAccountViewItems = new ObservableTransformer<ConnectionPoolState, List<AccountViewItem>>() {
@Override
public ObservableSource<List<AccountViewItem>> apply(Observable<ConnectionPoolState> upstream) {
return upstream.map(state -> {

View File

@ -0,0 +1,12 @@
package org.mercury_im.messenger.core.viewmodel.ox;
public enum OxBackupRestoreError {
invalid_backup_code,
no_backup_found,
invalid_user_id_on_key,
pgp_error,
not_authenticated_error,
protocol_error,
other_error
}

View File

@ -0,0 +1,70 @@
package org.mercury_im.messenger.core.viewmodel.ox;
import org.bouncycastle.openpgp.PGPException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
import org.jivesoftware.smackx.ox.exception.NoBackupFoundException;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
public class OxSecretKeyBackupRestoreViewModel implements MercuryViewModel {
private static final Logger LOGGER = Logger.getLogger(OxSecretKeyBackupRestoreViewModel.class.getName());
private final OpenPgpManager openPgpManager;
private BehaviorSubject<Optional<OxBackupRestoreError>> backupRestoreError = BehaviorSubject.createDefault(new Optional<>());
private BehaviorSubject<Boolean> finished = BehaviorSubject.createDefault(false);
@Inject
public OxSecretKeyBackupRestoreViewModel(OpenPgpManager openPgpManager) {
this.openPgpManager = openPgpManager;
}
public Observable<Optional<OxBackupRestoreError>> observeBackupRestoreError() {
return backupRestoreError;
}
public void onRestoreCodeEntered(String code) {
try {
OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(() -> code);
LOGGER.log(Level.INFO, "Successfully restored OX secret key " + fingerprint + " for " + openPgpManager.getOpenPgpSelf().getJid());
finished.onNext(true);
} catch (InvalidBackupCodeException e) {
LOGGER.log(Level.FINE, "Invalid backup code entered.");
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.invalid_backup_code));
} catch (MissingUserIdOnKeyException e) {
LOGGER.log(Level.WARNING, "Invalid or missing user ID on key.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.invalid_user_id_on_key));
} catch (NoBackupFoundException e) {
LOGGER.log(Level.FINE, "No secret key backup found.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.no_backup_found));
} catch (PGPException e) {
LOGGER.log(Level.WARNING, "PGP error while restoring key from server backup.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.pgp_error));
} catch (InterruptedException | IOException e) {
LOGGER.log(Level.SEVERE, "Error occurred while restoring key.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.other_error));
} catch (PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | SmackException.NoResponseException e) {
LOGGER.log(Level.WARNING, "Network related error encountered while restoring OX secret key.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.protocol_error));
} catch (SmackException.NotConnectedException | SmackException.NotLoggedInException e) {
LOGGER.log(Level.FINE, "Cannot restore OX secret key while not logged in.", e);
backupRestoreError.onNext(new Optional<>(OxBackupRestoreError.not_authenticated_error));
}
}
}

View File

@ -26,7 +26,7 @@ import lombok.Getter;
public class MercuryConnection {
private static final Logger LOGGER = Logger.getLogger("MercuryConnection");
private static final Logger LOGGER = Logger.getLogger(MercuryConnection.class.getName());
@Getter
private XMPPConnection connection;
@ -34,13 +34,13 @@ public class MercuryConnection {
@Getter
private final Account account;
private final BehaviorSubject<ConnectionState> state;
private final BehaviorSubject<ConnectionState> stateObservable;
public MercuryConnection(XMPPConnection connection, Account account) {
this.connection = connection;
this.account = account;
this.state = BehaviorSubject.createDefault(new ConnectionState(account.getId(), this,
this.stateObservable = BehaviorSubject.createDefault(new ConnectionState(account.getId(), this,
ConnectivityState.disconnected, false, false));
connection.addConnectionListener(connectionListener);
}
@ -50,7 +50,7 @@ public class MercuryConnection {
}
public Observable<ConnectionState> observeConnection() {
return state;
return stateObservable;
}
public Completable connect() {
@ -58,14 +58,12 @@ public class MercuryConnection {
.doOnError(error -> LOGGER.log(Level.WARNING, "Connection error for account " + account, error));
}
private void doConnect() throws ServerUnreachableException {
state.onNext(state.getValue().withConnectivity(ConnectivityState.connecting));
private synchronized void doConnect() throws ServerUnreachableException {
AbstractXMPPConnection connection = (AbstractXMPPConnection) getConnection();
if (connection.isConnected()) {
return;
}
try {
LOGGER.log(Level.INFO, "Connected!");
connection.connect();
} catch (SmackException.EndpointConnectionException e) {
connection.disconnect();
@ -73,6 +71,8 @@ public class MercuryConnection {
} catch (IOException | InterruptedException | XMPPException | SmackException e) {
throw new AssertionError("Unexpected exception.", e);
}
stateObservable.onNext(stateObservable.getValue().withConnectivity(ConnectivityState.connecting));
LOGGER.log(Level.INFO, "Connected!");
}
public Completable login() {
@ -80,7 +80,7 @@ public class MercuryConnection {
.doOnError(error -> LOGGER.log(Level.WARNING, "Login error for account " + account, error));
}
private void doLogin() throws InvalidCredentialsException {
private synchronized void doLogin() throws InvalidCredentialsException {
if (connection.isAuthenticated()) {
return;
}
@ -98,7 +98,7 @@ public class MercuryConnection {
.doOnError(error -> LOGGER.log(Level.WARNING, "Shutdown error for account " + account, error));
}
public void doShutdown() {
public synchronized void doShutdown() {
if (connection.isConnected()) {
((AbstractXMPPConnection) getConnection()).disconnect();
} else {
@ -109,32 +109,32 @@ public class MercuryConnection {
private final ConnectionListener connectionListener = new ConnectionListener() {
@Override
public void connected(XMPPConnection connection) {
state.onNext(state.getValue()
stateObservable.onNext(stateObservable.getValue()
.withConnectivity(ConnectivityState.connected)
.withAuthenticated(false));
}
@Override
public void authenticated(XMPPConnection connection, boolean resumed) {
state.onNext(state.getValue()
public void authenticated(XMPPConnection connection, boolean isResumed) {
stateObservable.onNext(stateObservable.getValue()
.withConnectivity(ConnectivityState.connected)
.withAuthenticated(true)
.withResumed(resumed));
if (!resumed) {
.withResumed(isResumed));
if (!isResumed) {
initialConnectionSetup();
}
}
@Override
public void connectionClosed() {
state.onNext(state.getValue()
stateObservable.onNext(stateObservable.getValue()
.withConnectivity(ConnectivityState.disconnected)
.withAuthenticated(false));
}
@Override
public void connectionClosedOnError(Exception e) {
state.onNext(state.getValue()
stateObservable.onNext(stateObservable.getValue()
.withConnectivity(ConnectivityState.disconnected)
.withAuthenticated(false));
}

View File

@ -86,13 +86,13 @@ public class MercuryConnectionManager {
start();
}
public void start() {
public synchronized void start() {
disposable.add(accountRepository.observeAllAccounts()
.subscribeOn(schedulers.getIoScheduler())
.subscribe(this::doRegisterConnections));
}
public List<MercuryConnection> getConnections() {
public synchronized List<MercuryConnection> getConnections() {
return new ArrayList<>(connectionsMap.values());
}
@ -101,19 +101,19 @@ public class MercuryConnectionManager {
}
public MercuryConnection getConnection(Account account) {
public synchronized MercuryConnection getConnection(Account account) {
return getConnection(account.getId());
}
public MercuryConnection getConnection(UUID id) {
public synchronized MercuryConnection getConnection(UUID id) {
return connectionsMap.get(id);
}
public MercuryConnection createConnection(Account account) {
public MercuryConnection createConnection(Account account) {
return new MercuryConnection(connectionFactory.createConnection(account), account);
}
public void doRegisterConnections(List<Account> accounts) {
public synchronized void doRegisterConnections(List<Account> accounts) {
for (Account account : accounts) {
if (!connectionsMap.containsKey(account.getId())) {
MercuryConnection connection = createConnection(account);
@ -126,7 +126,7 @@ public class MercuryConnectionManager {
return Completable.fromAction(() -> doRegisterConnection(connection));
}
public void doRegisterConnection(MercuryConnection connection) {
public synchronized void doRegisterConnection(MercuryConnection connection) {
LOGGER.log(Level.INFO, "Register Connection " + connection.getAccountId());
putConnection(connection);
disposable.add(accountRepository
@ -137,20 +137,25 @@ public class MercuryConnectionManager {
handleOptionalAccountChangedEvent(connection, event)));
}
private void putConnection(MercuryConnection connection) {
private synchronized void putConnection(MercuryConnection connection) {
connectionsMap.put(connection.getAccountId(), connection);
connectionDisposables.put(connection.getAccountId(), connection.observeConnection().subscribe(s ->
connectionPoolObservable.onNext(updatePoolState(connectionPoolObservable.getValue(), s))));
connectionDisposables.put(connection.getAccountId(), connection.observeConnection()
.subscribe(this::insertConnectionToPoolState));
bindConnection(connection);
}
private ConnectionPoolState updatePoolState(ConnectionPoolState poolState, ConnectionState conState) {
private void insertConnectionToPoolState(ConnectionState s) {
LOGGER.log(Level.INFO, "Insert new connection to pool state: " + s);
connectionPoolObservable.onNext(updatePoolState(connectionPoolObservable.getValue(), s));
}
private synchronized ConnectionPoolState updatePoolState(ConnectionPoolState poolState, ConnectionState conState) {
Map<UUID, ConnectionState> states = poolState.getConnectionStates();
states.put(conState.getId(), conState);
return new ConnectionPoolState(states);
}
public void bindConnection(MercuryConnection connection) {
public synchronized void bindConnection(MercuryConnection connection) {
rosterStoreBinder.setRosterStoreOn(connection);
disposable.add(accountRepository.getAccount(connection.getAccountId())
.subscribeOn(schedulers.getIoScheduler())
@ -164,14 +169,16 @@ public class MercuryConnectionManager {
}
private void handleOptionalAccountChangedEvent(MercuryConnection connection, Optional<Account> event) {
if (event.isPresent()) {
handleAccountChangedEvent(connection, event.getItem());
} else {
handleAccountRemoved(connection);
synchronized (connection) {
if (event.isPresent()) {
handleAccountChangedEvent(connection, event.getItem());
} else {
handleAccountRemoved(connection);
}
}
}
private void handleAccountChangedEvent(MercuryConnection connection, Account account) {
private synchronized void handleAccountChangedEvent(MercuryConnection connection, Account account) {
if (account.isEnabled()) {
handleAccountEnabled(connection);
} else {
@ -179,41 +186,46 @@ public class MercuryConnectionManager {
}
}
private void handleAccountDisabled(MercuryConnection connection) {
private synchronized void handleAccountDisabled(MercuryConnection connection) {
LOGGER.log(Level.FINER, "HandleAccountDisabled: " + connection.getAccountId());
disposable.add(connection.shutdown().subscribeOn(Schedulers.newThread()).subscribe());
}
private void handleAccountEnabled(MercuryConnection connection) {
private synchronized void handleAccountEnabled(MercuryConnection connection) {
LOGGER.log(Level.FINER, "HandleAccountEnabled: " + connection.getAccountId());
connectionLogin(connection);
}
private void connectionLogin(MercuryConnection connection) {
private synchronized void connectionLogin(MercuryConnection connection) {
disposable.add(connection.connect().andThen(connection.login())
.subscribeOn(Schedulers.io())
.subscribe(() -> LOGGER.log(Level.FINER, "Logged in."),
error -> LOGGER.log(Level.SEVERE, "Connection error!", error)));
}
private void handleAccountRemoved(MercuryConnection connection) {
private synchronized void handleAccountRemoved(MercuryConnection connection) {
LOGGER.log(Level.FINER, "HandleAccountRemove: " + connection.getAccountId());
disconnectAndRemoveConnection(connection);
}
private void disconnectAndRemoveConnection(MercuryConnection connection) {
private synchronized void disconnectAndRemoveConnection(MercuryConnection connection) {
disposable.add(connection.shutdown().subscribeOn(Schedulers.newThread()).subscribe());
removeConnection(connection);
}
private void removeConnection(MercuryConnection connection) {
private synchronized void removeConnection(MercuryConnection connection) {
LOGGER.log(Level.FINER, "Remove Connection: " + connection.getAccountId());
connectionsMap.remove(connection.getAccountId());
connectionDisposables.remove(connection.getAccountId()).dispose();
removeConnectionFromPoolState();
}
private void removeConnectionFromPoolState() {
LOGGER.log(Level.INFO, "Remove connection from pool state");
connectionPoolObservable.onNext(updatePoolState(connectionPoolObservable.getValue()));
}
private ConnectionPoolState updatePoolState(ConnectionPoolState value) {
private synchronized ConnectionPoolState updatePoolState(ConnectionPoolState value) {
Map<UUID, ConnectionState> states = value.getConnectionStates();
for (UUID id : connectionsMap.keySet()) {
if (!states.containsKey(id)) {
@ -223,7 +235,7 @@ public class MercuryConnectionManager {
return new ConnectionPoolState(states);
}
public void doShutdownAllConnections() {
public synchronized void doShutdownAllConnections() {
for (MercuryConnection connection : getConnections()) {
connection.doShutdown();
}

View File

@ -12,7 +12,7 @@ import org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager;
public class SmackConfig {
static void staticConfiguration() {
SmackConfiguration.DEBUG = true;
SmackConfiguration.DEBUG = false;
ReconnectionManager.setEnabledPerDefault(true);
ReconnectionManager.setDefaultReconnectionPolicy(ReconnectionManager.ReconnectionPolicy.RANDOM_INCREASING_DELAY);