add UI for certificate trust
This commit is contained in:
parent
786a6c4c2a
commit
9c64f9c24c
|
@ -1,8 +1,8 @@
|
||||||
package im.conversations.android.ui;
|
package im.conversations.android.ui;
|
||||||
|
|
||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
@ -12,7 +12,7 @@ public final class NavControllers {
|
||||||
private NavControllers() {}
|
private NavControllers() {}
|
||||||
|
|
||||||
public static NavController findNavController(
|
public static NavController findNavController(
|
||||||
final AppCompatActivity activity, @IdRes int fragmentId) {
|
final FragmentActivity activity, @IdRes int fragmentId) {
|
||||||
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
|
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
|
||||||
final Fragment fragment = fragmentManager.findFragmentById(fragmentId);
|
final Fragment fragment = fragmentManager.findFragmentById(fragmentId);
|
||||||
if (fragment instanceof NavHostFragment) {
|
if (fragment instanceof NavHostFragment) {
|
||||||
|
|
|
@ -55,6 +55,13 @@ public class SetupActivity extends BaseActivity {
|
||||||
case ENTER_HOSTNAME:
|
case ENTER_HOSTNAME:
|
||||||
navController.navigate(SetupNavigationDirections.enterHostname());
|
navController.navigate(SetupNavigationDirections.enterHostname());
|
||||||
break;
|
break;
|
||||||
|
case TRUST_CERTIFICATE:
|
||||||
|
final var currentDestination = navController.getCurrentDestination();
|
||||||
|
if (currentDestination == null
|
||||||
|
|| currentDestination.getId() != R.id.certificate) {
|
||||||
|
navController.navigate(SetupNavigationDirections.trustCertificate());
|
||||||
|
}
|
||||||
|
break;
|
||||||
case DONE:
|
case DONE:
|
||||||
startActivity(new Intent(this, MainActivity.class));
|
startActivity(new Intent(this, MainActivity.class));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package im.conversations.android.ui.fragment.setup;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import androidx.activity.OnBackPressedCallback;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.databinding.DataBindingUtil;
|
||||||
|
import im.conversations.android.R;
|
||||||
|
import im.conversations.android.databinding.FragmentTrustCertificateBinding;
|
||||||
|
import im.conversations.android.ui.NavControllers;
|
||||||
|
|
||||||
|
public class TrustCertificateFragment extends AbstractSetupFragment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
FragmentTrustCertificateBinding binding =
|
||||||
|
DataBindingUtil.inflate(
|
||||||
|
inflater, R.layout.fragment_trust_certificate, container, false);
|
||||||
|
binding.setSetupViewModel(setupViewModel);
|
||||||
|
binding.setLifecycleOwner(getViewLifecycleOwner());
|
||||||
|
requireActivity()
|
||||||
|
.getOnBackPressedDispatcher()
|
||||||
|
.addCallback(
|
||||||
|
getViewLifecycleOwner(),
|
||||||
|
new OnBackPressedCallback(true) {
|
||||||
|
@Override
|
||||||
|
public void handleOnBackPressed() {
|
||||||
|
setupViewModel.rejectTrustDecision();
|
||||||
|
NavControllers.findNavController(
|
||||||
|
requireActivity(), R.id.nav_host_fragment)
|
||||||
|
.navigateUp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return binding.getRoot();
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,8 +26,11 @@ import im.conversations.android.xmpp.ConnectionPool;
|
||||||
import im.conversations.android.xmpp.ConnectionState;
|
import im.conversations.android.xmpp.ConnectionState;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.manager.TrustManager;
|
import im.conversations.android.xmpp.manager.TrustManager;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jxmpp.jid.BareJid;
|
import org.jxmpp.jid.BareJid;
|
||||||
|
@ -56,13 +59,24 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
private final MutableLiveData<Event<Target>> redirection = new MutableLiveData<>();
|
||||||
|
|
||||||
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
|
private final MutableLiveData<TrustDecision> trustDecision = new MutableLiveData<>();
|
||||||
|
private final HashMap<ByteBuffer, Boolean> trustDecisions = new HashMap<>();
|
||||||
|
|
||||||
private final Function<byte[], ListenableFuture<Boolean>> trustDecisionCallback =
|
private final Function<byte[], ListenableFuture<Boolean>> trustDecisionCallback =
|
||||||
fingerprint -> {
|
fingerprint -> {
|
||||||
|
final var decision = this.trustDecisions.get(ByteBuffer.wrap(fingerprint));
|
||||||
|
if (decision != null) {
|
||||||
|
LOGGER.info("Using previous trust decision ({})", decision);
|
||||||
|
return Futures.immediateFuture(decision);
|
||||||
|
}
|
||||||
|
LOGGER.info("Trust decision arrived in UI");
|
||||||
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
final SettableFuture<Boolean> settableFuture = SettableFuture.create();
|
||||||
final var trustDecision = new TrustDecision(fingerprint, settableFuture);
|
final var trustDecision = new TrustDecision(fingerprint, settableFuture);
|
||||||
LOGGER.debug("posting trust decision");
|
final var currentOperation = this.currentOperation;
|
||||||
|
if (currentOperation != null) {
|
||||||
|
currentOperation.cancel(false);
|
||||||
|
}
|
||||||
this.trustDecision.postValue(trustDecision);
|
this.trustDecision.postValue(trustDecision);
|
||||||
|
this.redirection.postValue(new Event<>(Target.TRUST_CERTIFICATE));
|
||||||
return settableFuture;
|
return settableFuture;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,7 +157,7 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
this.xmppAddressError.postValue(getApplication().getString(R.string.invalid_jid));
|
this.xmppAddressError.postValue(getApplication().getString(R.string.invalid_jid));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
this.trustDecisions.clear();
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
if (account.address.equals(address)) {
|
if (account.address.equals(address)) {
|
||||||
this.accountRepository.reconnect(account);
|
this.accountRepository.reconnect(account);
|
||||||
|
@ -167,6 +181,51 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean trustCertificate() {
|
||||||
|
final var trustDecision = this.trustDecision.getValue();
|
||||||
|
final var account = this.account;
|
||||||
|
if (trustDecision == null || account == null) {
|
||||||
|
// TODO navigate back to sign in or show error?
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOGGER.info(
|
||||||
|
"trying to commit trust for fingerprint {}",
|
||||||
|
TrustManager.fingerprint(trustDecision.fingerprint));
|
||||||
|
// in case the UI interface hook gets called again before this gets written to DB
|
||||||
|
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), true);
|
||||||
|
if (trustDecision.decision.isDone()) {
|
||||||
|
ConnectionPool.getInstance(getApplication()).reconnect(account);
|
||||||
|
LOGGER.info("it was already done. we should reconnect");
|
||||||
|
}
|
||||||
|
trustDecision.decision.set(true);
|
||||||
|
decideNextStep(Target.TRUST_CERTIFICATE, account);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rejectTrustDecision() {
|
||||||
|
final var trustDecision = this.trustDecision.getValue();
|
||||||
|
if (trustDecision == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOGGER.info(
|
||||||
|
"Rejecting trust decision for {}",
|
||||||
|
TrustManager.fingerprint(trustDecision.fingerprint));
|
||||||
|
trustDecision.decision.set(false);
|
||||||
|
this.trustDecisions.put(ByteBuffer.wrap(trustDecision.fingerprint), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getFingerprint() {
|
||||||
|
return Transformations.map(
|
||||||
|
this.trustDecision,
|
||||||
|
td -> {
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return TrustManager.fingerprint(td.fingerprint, 8);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void createAccount(final BareJid address) {
|
private void createAccount(final BareJid address) {
|
||||||
|
|
||||||
// if the user hasn't entered anything we want this to be null so we don't store credentials
|
// if the user hasn't entered anything we want this to be null so we don't store credentials
|
||||||
|
@ -229,6 +288,8 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
final var optionalTrustManager = getTrustManager();
|
final var optionalTrustManager = getTrustManager();
|
||||||
if (optionalTrustManager.isPresent()) {
|
if (optionalTrustManager.isPresent()) {
|
||||||
optionalTrustManager.get().removeUserInterfaceCallback(this.trustDecisionCallback);
|
optionalTrustManager.get().removeUserInterfaceCallback(this.trustDecisionCallback);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("No trust manager found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,6 +304,7 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
public void onSuccess(final XmppConnection result) {
|
public void onSuccess(final XmppConnection result) {
|
||||||
// TODO only when configured for loginAndBind
|
// TODO only when configured for loginAndBind
|
||||||
LOGGER.info("Account setup successful");
|
LOGGER.info("Account setup successful");
|
||||||
|
unregisterTrustDecisionCallback();
|
||||||
SetupViewModel.this.account = null;
|
SetupViewModel.this.account = null;
|
||||||
redirect(Target.DONE);
|
redirect(Target.DONE);
|
||||||
}
|
}
|
||||||
|
@ -250,6 +312,10 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull final Throwable throwable) {
|
public void onFailure(@NonNull final Throwable throwable) {
|
||||||
loading.postValue(false);
|
loading.postValue(false);
|
||||||
|
if (throwable instanceof CancellationException) {
|
||||||
|
LOGGER.info("connection future was cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (throwable instanceof ConnectionException) {
|
if (throwable instanceof ConnectionException) {
|
||||||
decideNextStep(current, ((ConnectionException) throwable));
|
decideNextStep(current, ((ConnectionException) throwable));
|
||||||
} else {
|
} else {
|
||||||
|
@ -317,6 +383,7 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
this.portError.postValue(getApplication().getString(R.string.invalid));
|
this.portError.postValue(getApplication().getString(R.string.invalid));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
this.trustDecisions.clear();
|
||||||
final boolean directTls = Boolean.FALSE.equals(this.opportunisticTls.getValue());
|
final boolean directTls = Boolean.FALSE.equals(this.opportunisticTls.getValue());
|
||||||
final var connection = new Connection(hostname, port, directTls);
|
final var connection = new Connection(hostname, port, directTls);
|
||||||
final var setConnectionFuture =
|
final var setConnectionFuture =
|
||||||
|
@ -388,15 +455,19 @@ public class SetupViewModel extends AndroidViewModel {
|
||||||
return this.redirection;
|
return this.redirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onCleared() {
|
public void onCleared() {
|
||||||
super.onCleared();
|
LOGGER.info("Clearing view model");
|
||||||
this.unregisterTrustDecisionCallback();
|
this.unregisterTrustDecisionCallback();
|
||||||
|
super.onCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Target {
|
public enum Target {
|
||||||
ENTER_ADDRESS,
|
ENTER_ADDRESS,
|
||||||
ENTER_PASSWORD,
|
ENTER_PASSWORD,
|
||||||
ENTER_HOSTNAME,
|
ENTER_HOSTNAME,
|
||||||
|
|
||||||
|
TRUST_CERTIFICATE,
|
||||||
DONE
|
DONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
package im.conversations.android.util;
|
package im.conversations.android.util;
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class PendingItem<T> {
|
public class PendingItem<T> {
|
||||||
|
|
||||||
|
@ -49,10 +49,9 @@ public class PendingItem<T> {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized T peekOrCreate(final Supplier<T> supplier) {
|
public synchronized T peekOrSwap(final Function<T, T> swap) {
|
||||||
if (this.item == null) {
|
final T item = this.item;
|
||||||
this.item = supplier.get();
|
this.item = swap.apply(item);
|
||||||
}
|
|
||||||
return this.item;
|
return this.item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1882,7 +1882,14 @@ public class XmppConnection implements Runnable {
|
||||||
} else if (Arrays.asList(ConnectionState.OFFLINE, ConnectionState.CONNECTING)
|
} else if (Arrays.asList(ConnectionState.OFFLINE, ConnectionState.CONNECTING)
|
||||||
.contains(state)
|
.contains(state)
|
||||||
|| waitOnError) {
|
|| waitOnError) {
|
||||||
return this.connectedFuture.peekOrCreate(SettableFuture::create);
|
return this.connectedFuture.peekOrSwap(
|
||||||
|
f -> {
|
||||||
|
if (f == null || f.isDone()) {
|
||||||
|
return SettableFuture.create();
|
||||||
|
} else {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return Futures.immediateFailedFuture(new ConnectionException(state));
|
return Futures.immediateFailedFuture(new ConnectionException(state));
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
|
||||||
try {
|
try {
|
||||||
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
|
decision = Boolean.TRUE.equals(futureDecision.get(10, TimeUnit.SECONDS));
|
||||||
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
|
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
|
||||||
|
futureDecision.cancel(true);
|
||||||
throw new CertificateException(
|
throw new CertificateException(
|
||||||
"Timeout waiting for user response", Throwables.getRootCause(e));
|
"Timeout waiting for user response", Throwables.getRootCause(e));
|
||||||
}
|
}
|
||||||
|
@ -97,8 +98,17 @@ public class TrustManager extends AbstractManager implements X509TrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String fingerprint(final byte[] bytes) {
|
public static String fingerprint(final byte[] bytes) {
|
||||||
return Joiner.on(':')
|
return fingerprint(bytes, bytes.length);
|
||||||
.join(Lists.transform(Bytes.asList(bytes), b -> String.format("%02X", b)));
|
}
|
||||||
|
|
||||||
|
public static String fingerprint(final byte[] bytes, final int segments) {
|
||||||
|
return Joiner.on('\n')
|
||||||
|
.join(
|
||||||
|
Lists.transform(
|
||||||
|
Lists.transform(
|
||||||
|
Lists.partition(Bytes.asList(bytes), segments),
|
||||||
|
s -> Lists.transform(s, b -> String.format("%02X", b))),
|
||||||
|
hex -> Joiner.on(':').join(hex)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserInterfaceCallback(
|
public void setUserInterfaceCallback(
|
||||||
|
|
142
app/src/main/res/layout/fragment_trust_certificate.xml
Normal file
142
app/src/main/res/layout/fragment_trust_certificate.xml
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright 2019-2023 Daniel Gultsch
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<layout 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">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/app_bar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:liftOnScroll="false"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/material_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="@string/app_name"
|
||||||
|
app:menu="@menu/activity_setup"
|
||||||
|
app:titleTextColor="?colorPrimary"
|
||||||
|
app:titleCentered="true" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="@{setupViewModel.isLoading() ? View.VISIBLE : View.INVISIBLE}" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:minHeight="@dimen/setup_main_min_height"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/setup_screen_padding">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/header"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:textAppearance="?textAppearanceTitleMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="@dimen/setup_screen_top_margin"
|
||||||
|
android:text="@string/potential_security_risk_ahead" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/warning"
|
||||||
|
android:textAppearance="?textAppearanceBodyMedium"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:layout_below="@+id/header"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_marginTop="12sp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="@string/trust_certificate_warning"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?textAppearanceBodyMedium"
|
||||||
|
android:layout_below="@+id/warning"
|
||||||
|
android:id="@+id/instructions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12sp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:text="@string/trust_certificate_instructions" />
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_marginVertical="24dp"
|
||||||
|
android:id="@+id/password_input_layout"
|
||||||
|
android:layout_width="488dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_below="@+id/instructions"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:layout_gravity="center_horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?textAppearanceBodyLarge"
|
||||||
|
android:textColor="?colorTertiary"
|
||||||
|
android:typeface="monospace"
|
||||||
|
android:text="@{setupViewModel.fingerprint}"
|
||||||
|
tools:text="2B:F0:58:07:E1:A9:7F:4E\nD5:90:90:98:D5:61:39:12\n90:21:8D:83:C8:52:CB:82\n24:28:B5:43:2F:AE:81:44" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:enabled="@{!setupViewModel.isLoading()}"
|
||||||
|
android:onClick="@{(v) -> setupViewModel.trustCertificate()}"
|
||||||
|
android:text="@string/trust_cerficate" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="setupViewModel"
|
||||||
|
type="im.conversations.android.ui.model.SetupViewModel" />
|
||||||
|
</data>
|
||||||
|
</layout>
|
|
@ -28,6 +28,15 @@
|
||||||
app:popExitAnim="@anim/slide_to_right"
|
app:popExitAnim="@anim/slide_to_right"
|
||||||
app:popUpTo="@+id/hostname" />
|
app:popUpTo="@+id/hostname" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/trustCertificate"
|
||||||
|
app:destination="@+id/certificate"
|
||||||
|
app:enterAnim="@anim/slide_from_right"
|
||||||
|
app:exitAnim="@anim/slide_to_left"
|
||||||
|
app:popEnterAnim="@anim/slide_from_left"
|
||||||
|
app:popExitAnim="@anim/slide_to_right"
|
||||||
|
app:popUpTo="@+id/hostname" />
|
||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/enterHostname"
|
android:id="@+id/enterHostname"
|
||||||
app:destination="@+id/hostname"
|
app:destination="@+id/hostname"
|
||||||
|
@ -48,6 +57,10 @@
|
||||||
android:name="im.conversations.android.ui.fragment.setup.HostnameFragment"
|
android:name="im.conversations.android.ui.fragment.setup.HostnameFragment"
|
||||||
tools:layout="@layout/fragment_hostname" />
|
tools:layout="@layout/fragment_hostname" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/certificate"
|
||||||
|
android:name="im.conversations.android.ui.fragment.setup.TrustCertificateFragment" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/password"
|
android:id="@+id/password"
|
||||||
android:name="im.conversations.android.ui.fragment.setup.PasswordFragment"
|
android:name="im.conversations.android.ui.fragment.setup.PasswordFragment"
|
||||||
|
|
|
@ -1042,5 +1042,9 @@
|
||||||
<string name="info_required">Info required</string>
|
<string name="info_required">Info required</string>
|
||||||
<string name="invalid">Invalid!</string>
|
<string name="invalid">Invalid!</string>
|
||||||
<string name="certificate_login">Certificate login</string>
|
<string name="certificate_login">Certificate login</string>
|
||||||
|
<string name="potential_security_risk_ahead">Potential security risk ahead</string>
|
||||||
|
<string name="trust_certificate_instructions">To continue compare this SHA-256 fingerprint with that of the server certificate</string>
|
||||||
|
<string name="trust_certificate_warning">The server certificate is not trustworthy. If you don’t know what this means it’s best to go back!</string>
|
||||||
|
<string name="trust_cerficate">Trust certificate</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue