Merge branch '2172-mailbox-status-ui' into 'master'

Implement status UI for mailbox connection

Closes #2172

See merge request briar/briar!1617
This commit is contained in:
akwizgran
2022-04-14 12:46:28 +00:00
10 changed files with 325 additions and 57 deletions

View File

@@ -32,4 +32,13 @@ public interface MailboxManager {
*/
MailboxPairingTask startPairingTask(String qrCodePayload);
/**
* Can be used by the UI to test the mailbox connection.
*
* @return true (success) or false (error).
* A {@link OwnMailboxConnectionStatusEvent} might be broadcast with a new
* {@link MailboxStatus}.
*/
boolean checkConnection();
}

View File

@@ -2,27 +2,42 @@ package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.mailbox.MailboxManager;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@Immutable
@NotNullByDefault
class MailboxManagerImpl implements MailboxManager {
private static final String TAG = MailboxManagerImpl.class.getName();
private final static Logger LOG = getLogger(TAG);
private final Executor ioExecutor;
private final MailboxApi api;
private final TransactionManager db;
private final MailboxSettingsManager mailboxSettingsManager;
private final MailboxPairingTaskFactory pairingTaskFactory;
private final Clock clock;
private final Object lock = new Object();
@Nullable
@@ -32,11 +47,17 @@ class MailboxManagerImpl implements MailboxManager {
@Inject
MailboxManagerImpl(
@IoExecutor Executor ioExecutor,
MailboxApi api,
TransactionManager db,
MailboxSettingsManager mailboxSettingsManager,
MailboxPairingTaskFactory pairingTaskFactory) {
MailboxPairingTaskFactory pairingTaskFactory,
Clock clock) {
this.ioExecutor = ioExecutor;
this.api = api;
this.db = db;
this.mailboxSettingsManager = mailboxSettingsManager;
this.pairingTaskFactory = pairingTaskFactory;
this.clock = clock;
}
@Override
@@ -75,4 +96,29 @@ class MailboxManagerImpl implements MailboxManager {
return created;
}
@Override
public boolean checkConnection() {
boolean success;
try {
MailboxProperties props = db.transactionWithNullableResult(true,
mailboxSettingsManager::getOwnMailboxProperties);
success = api.checkStatus(props);
} catch (DbException | IOException | MailboxApi.ApiException e) {
success = false;
logException(LOG, WARNING, e);
}
if (success) {
try {
// we are only recording successful connections here
// as those update the UI and failures might be false negatives
db.transaction(false, txn ->
mailboxSettingsManager.recordSuccessfulConnection(txn,
clock.currentTimeMillis()));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
return success;
}
}

View File

@@ -47,11 +47,11 @@ public class MailboxActivity extends BriarActivity {
setContentView(R.layout.activity_mailbox);
progressBar = findViewById(R.id.progressBar);
if (viewModel.getState().getValue() == null) {
if (viewModel.getPairingState().getValue() == null) {
progressBar.setVisibility(VISIBLE);
}
viewModel.getState().observeEvent(this, state -> {
viewModel.getPairingState().observeEvent(this, state -> {
if (state instanceof MailboxState.NotSetup) {
onNotSetup();
} else if (state instanceof MailboxState.ShowDownload) {
@@ -67,7 +67,7 @@ public class MailboxActivity extends BriarActivity {
} else if (state instanceof MailboxState.CameraError) {
onCameraError();
} else if (state instanceof MailboxState.IsPaired) {
onIsPaired();
onIsPaired(((MailboxState.IsPaired) state).isOnline);
} else {
throw new AssertionError("Unknown state: " + state);
}
@@ -85,7 +85,7 @@ public class MailboxActivity extends BriarActivity {
@Override
public void onBackPressed() {
MailboxState s = viewModel.getState().getLastValue();
MailboxState s = viewModel.getPairingState().getLastValue();
if (s instanceof MailboxState.Pairing) {
// don't go back in the flow if we are already pairing
// with the mailbox. We provide a try-again button instead.
@@ -181,10 +181,13 @@ public class MailboxActivity extends BriarActivity {
showFragment(getSupportFragmentManager(), f, ErrorFragment.TAG);
}
private void onIsPaired() {
private void onIsPaired(boolean isOnline) {
progressBar.setVisibility(INVISIBLE);
showFragment(getSupportFragmentManager(), new MailboxStatusFragment(),
MailboxStatusFragment.TAG, false);
Fragment f = isOnline ?
new MailboxStatusFragment() : new OfflineStatusFragment();
String tag = isOnline ?
MailboxStatusFragment.TAG : OfflineStatusFragment.TAG;
showFragment(getSupportFragmentManager(), f, tag, false);
}
}

View File

@@ -1,7 +1,6 @@
package org.briarproject.briar.android.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxPairingState;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
class MailboxState {
@@ -29,10 +28,10 @@ class MailboxState {
}
static class IsPaired extends MailboxState {
final MailboxStatus mailboxStatus;
final boolean isOnline;
IsPaired(MailboxStatus mailboxStatus) {
this.mailboxStatus = mailboxStatus;
IsPaired(boolean isOnline) {
this.isOnline = isOnline;
}
}

View File

@@ -1,37 +1,62 @@
package org.briarproject.briar.android.mailbox;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.widget.ImageViewCompat.setImageTintList;
import static androidx.transition.TransitionManager.beginDelayedTransition;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class MailboxStatusFragment extends Fragment {
static final String TAG = MailboxStatusFragment.class.getName();
private static final int NUM_FAILURES = 4;
@Inject
ViewModelProvider.Factory viewModelFactory;
private MailboxViewModel viewModel;
private final Handler handler = new Handler(Looper.getMainLooper());
@Nullable // UiThread
private Runnable refresher = null;
private ImageView imageView;
private TextView statusTitleView;
private TextView statusInfoView;
@Override
public void onAttach(Context context) {
@@ -54,11 +79,72 @@ public class MailboxStatusFragment extends Fragment {
@Override
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
MailboxState.IsPaired state =
(MailboxState.IsPaired) viewModel.getState().getLastValue();
requireNonNull(state); // TODO check assumption
TextView statusInfoView = v.findViewById(R.id.statusInfoView);
long lastSuccess = state.mailboxStatus.getTimeOfLastSuccess();
Button checkButton = v.findViewById(R.id.checkButton);
ProgressBar checkProgress = v.findViewById(R.id.checkProgress);
checkButton.setOnClickListener(view -> {
beginDelayedTransition((ViewGroup) v);
checkButton.setVisibility(INVISIBLE);
checkProgress.setVisibility(VISIBLE);
observeOnce(viewModel.checkConnection(), this, result -> {
beginDelayedTransition((ViewGroup) v);
checkButton.setVisibility(VISIBLE);
checkProgress.setVisibility(INVISIBLE);
});
});
imageView = v.findViewById(R.id.imageView);
statusTitleView = v.findViewById(R.id.statusTitleView);
statusInfoView = v.findViewById(R.id.statusInfoView);
viewModel.getStatus()
.observe(getViewLifecycleOwner(), this::onMailboxStateChanged);
// TODO
// * detect problems and show them #2175
// * add "Unlink" button confirmation dialog and functionality #2173
Button unlinkButton = v.findViewById(R.id.unlinkButton);
unlinkButton.setOnClickListener(view -> Toast.makeText(requireContext(),
"NOT IMPLEMENTED", Toast.LENGTH_SHORT).show());
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.mailbox_status_title);
refresher = this::refreshLastConnection;
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
@Override
public void onStop() {
super.onStop();
handler.removeCallbacks(refresher);
refresher = null;
}
private void onMailboxStateChanged(MailboxStatus status) {
@ColorRes int tintRes;
@DrawableRes int iconRes;
String title;
if (status.getAttemptsSinceSuccess() == 0) {
iconRes = R.drawable.ic_check_circle_outline;
title = getString(R.string.mailbox_status_connected_title);
tintRes = R.color.briar_brand_green;
} else if (status.getAttemptsSinceSuccess() < NUM_FAILURES) {
iconRes = R.drawable.ic_help_outline_white;
title = getString(R.string.mailbox_status_problem_title);
tintRes = R.color.briar_orange_500;
} else {
tintRes = R.color.briar_red_500;
title = getString(R.string.mailbox_status_failure_title);
iconRes = R.drawable.alerts_and_states_error;
}
imageView.setImageResource(iconRes);
int color = getColor(requireContext(), tintRes);
setImageTintList(imageView, ColorStateList.valueOf(color));
statusTitleView.setText(title);
long lastSuccess = status.getTimeOfLastSuccess();
String lastConnectionText;
if (lastSuccess < 0) {
lastConnectionText =
@@ -66,21 +152,19 @@ public class MailboxStatusFragment extends Fragment {
} else {
lastConnectionText = formatDate(requireContext(), lastSuccess);
}
String statusInfoText = getString(
R.string.mailbox_status_connected_info, lastConnectionText);
String statusInfoText =
getString(R.string.mailbox_status_connected_info,
lastConnectionText);
statusInfoView.setText(statusInfoText);
// TODO
// * react to status changes
// * detect problems and show them
// * update connection time periodically like conversation timestamps
// * add "Check connection" button
// * add "Unlink" button with confirmation dialog
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.mailbox_status_title);
@UiThread
private void refreshLastConnection() {
MailboxStatus status = viewModel.getStatus().getValue();
if (status != null) onMailboxStateChanged(status);
if (refresher != null) {
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
}
}

View File

@@ -7,16 +7,22 @@ import com.google.zxing.Result;
import org.briarproject.bramble.api.Consumer;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.mailbox.MailboxManager;
import org.briarproject.bramble.api.mailbox.MailboxPairingState;
import org.briarproject.bramble.api.mailbox.MailboxPairingTask;
import org.briarproject.bramble.api.mailbox.MailboxStatus;
import org.briarproject.bramble.api.mailbox.OwnMailboxConnectionStatusEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.mailbox.MailboxState.NotSetup;
import org.briarproject.briar.android.qrcode.QrCodeDecoder;
@@ -32,6 +38,8 @@ import javax.inject.Inject;
import androidx.annotation.AnyThread;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
@@ -39,17 +47,22 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
@NotNullByDefault
class MailboxViewModel extends DbViewModel
implements QrCodeDecoder.ResultCallback, Consumer<MailboxPairingState> {
implements QrCodeDecoder.ResultCallback, Consumer<MailboxPairingState>,
EventListener {
private static final Logger LOG =
getLogger(MailboxViewModel.class.getName());
private final EventBus eventBus;
private final Executor ioExecutor;
private final QrCodeDecoder qrCodeDecoder;
private final PluginManager pluginManager;
private final MailboxManager mailboxManager;
private final MutableLiveEvent<MailboxState> state =
private final MutableLiveEvent<MailboxState> pairingState =
new MutableLiveEvent<>();
private final MutableLiveData<MailboxStatus> status =
new MutableLiveData<>();
@Nullable
private MailboxPairingTask pairingTask = null;
@@ -60,19 +73,24 @@ class MailboxViewModel extends DbViewModel
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
@IoExecutor Executor ioExecutor,
PluginManager pluginManager,
MailboxManager mailboxManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.eventBus = eventBus;
this.ioExecutor = ioExecutor;
this.pluginManager = pluginManager;
this.mailboxManager = mailboxManager;
qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
eventBus.addListener(this);
checkIfSetup();
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
MailboxPairingTask task = pairingTask;
if (task != null) {
task.removeObserver(this);
@@ -89,9 +107,11 @@ class MailboxViewModel extends DbViewModel
if (isPaired) {
MailboxStatus mailboxStatus =
mailboxManager.getMailboxStatus(txn);
state.postEvent(new MailboxState.IsPaired(mailboxStatus));
boolean isOnline = isTorActive();
pairingState.postEvent(new MailboxState.IsPaired(isOnline));
status.postValue(mailboxStatus);
} else {
state.postEvent(new NotSetup());
pairingState.postEvent(new NotSetup());
}
}, this::handleException);
} else {
@@ -100,18 +120,37 @@ class MailboxViewModel extends DbViewModel
}
}
@UiThread
@Override
public void eventOccurred(Event e) {
if (e instanceof OwnMailboxConnectionStatusEvent) {
MailboxStatus status =
((OwnMailboxConnectionStatusEvent) e).getStatus();
this.status.setValue(status);
} else if (e instanceof TransportInactiveEvent) {
TransportId id = ((TransportInactiveEvent) e).getTransportId();
if (!TorConstants.ID.equals(id)) return;
MailboxState lastState = pairingState.getLastValue();
if (lastState instanceof MailboxState.IsPaired) {
pairingState.setEvent(new MailboxState.IsPaired(false));
} else if (lastState != null) {
pairingState.setEvent(new MailboxState.OfflineWhenPairing());
}
}
}
@UiThread
void onScanButtonClicked() {
if (isTorActive()) {
state.setEvent(new MailboxState.ScanningQrCode());
pairingState.setEvent(new MailboxState.ScanningQrCode());
} else {
state.setEvent(new MailboxState.OfflineWhenPairing());
pairingState.setEvent(new MailboxState.OfflineWhenPairing());
}
}
@UiThread
void onCameraError() {
state.setEvent(new MailboxState.CameraError());
pairingState.setEvent(new MailboxState.CameraError());
}
@Override
@@ -127,7 +166,7 @@ class MailboxViewModel extends DbViewModel
pairingTask = mailboxManager.startPairingTask(qrCodePayload);
pairingTask.addObserver(this);
} else {
state.postEvent(new MailboxState.OfflineWhenPairing());
pairingState.postEvent(new MailboxState.OfflineWhenPairing());
}
}
@@ -138,7 +177,7 @@ class MailboxViewModel extends DbViewModel
LOG.info("New pairing state: " +
mailboxPairingState.getClass().getSimpleName());
}
state.setEvent(new MailboxState.Pairing(mailboxPairingState));
pairingState.setEvent(new MailboxState.Pairing(mailboxPairingState));
}
private boolean isTorActive() {
@@ -148,7 +187,7 @@ class MailboxViewModel extends DbViewModel
@UiThread
void showDownloadFragment() {
state.setEvent(new MailboxState.ShowDownload());
pairingState.setEvent(new MailboxState.ShowDownload());
}
@UiThread
@@ -157,7 +196,37 @@ class MailboxViewModel extends DbViewModel
}
@UiThread
LiveEvent<MailboxState> getState() {
return state;
void checkIfOnlineWhenPaired() {
boolean isOnline = isTorActive();
pairingState.setEvent(new MailboxState.IsPaired(isOnline));
}
LiveData<Boolean> checkConnection() {
MutableLiveData<Boolean> liveData = new MutableLiveData<>();
ioExecutor.execute(() -> {
boolean success = mailboxManager.checkConnection();
if (LOG.isLoggable(INFO)) {
LOG.info("Got result from connection check: " + success);
}
liveData.postValue(success);
if (!success) { // force failure screen
MailboxStatus lastStatus = status.getValue();
long lastSuccess = lastStatus == null ?
-1 : lastStatus.getTimeOfLastSuccess();
long now = System.currentTimeMillis();
status.postValue(new MailboxStatus(now, lastSuccess, 999));
}
});
return liveData;
}
@UiThread
LiveEvent<MailboxState> getPairingState() {
return pairingState;
}
@UiThread
LiveData<MailboxStatus> getStatus() {
return status;
}
}

View File

@@ -33,10 +33,9 @@ public class OfflineFragment extends Fragment {
@Inject
ViewModelProvider.Factory viewModelFactory;
private MailboxViewModel viewModel;
protected MailboxViewModel viewModel;
private NestedScrollView scrollView;
protected Button buttonView;
@Override
public void onAttach(Context context) {
@@ -61,8 +60,8 @@ public class OfflineFragment extends Fragment {
Intent i = new Intent(requireContext(), TransportsActivity.class);
startActivity(i);
});
buttonView = v.findViewById(R.id.button);
buttonView.setOnClickListener(view -> viewModel.showDownloadFragment());
Button buttonView = v.findViewById(R.id.button);
buttonView.setOnClickListener(view -> onTryAgainClicked());
return v;
}
@@ -74,4 +73,8 @@ public class OfflineFragment extends Fragment {
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
}
protected void onTryAgainClicked() {
viewModel.showDownloadFragment();
}
}

View File

@@ -0,0 +1,17 @@
package org.briarproject.briar.android.mailbox;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class OfflineStatusFragment extends OfflineFragment {
public static final String TAG = OfflineStatusFragment.class.getName();
@Override
protected void onTryAgainClicked() {
viewModel.checkIfOnlineWhenPaired();
}
}

View File

@@ -10,13 +10,14 @@
android:id="@+id/imageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/statusTitleView"
android:layout_marginHorizontal="16dp"
app:layout_constraintBottom_toTopOf="@+id/statusTitleView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_check_circle_outline"
app:tint="@color/briar_brand_green"
@@ -26,14 +27,37 @@
android:id="@+id/statusTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_margin="16dp"
android:text="@string/mailbox_status_connected_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@+id/imageView"
app:layout_constraintBottom_toTopOf="@+id/checkButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/checkButton"
style="@style/BriarButtonFlat.Neutral"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:text="@string/mailbox_status_check_button"
app:layout_constraintBottom_toTopOf="@+id/statusInfoView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/statusTitleView" />
<ProgressBar
android:id="@+id/checkProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/checkButton"
app:layout_constraintEnd_toEndOf="@+id/checkButton"
app:layout_constraintStart_toStartOf="@+id/checkButton"
app:layout_constraintTop_toTopOf="@+id/checkButton"
tools:visibility="visible" />
<TextView
android:id="@+id/statusInfoView"
@@ -41,11 +65,21 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/unlinkButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/statusTitleView"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintTop_toBottomOf="@+id/checkButton"
tools:text="@string/mailbox_status_connected_info" />
<Button
android:id="@+id/unlinkButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/mailbox_status_unlink_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -643,10 +643,14 @@
<string name="tor_offline_button_check">Check connection settings</string>
<string name="mailbox_status_title">Mailbox status</string>
<string name="mailbox_status_connected_title">Mailbox is running</string>
<string name="mailbox_status_problem_title">We are having trouble connecting to the mailbox</string>
<string name="mailbox_status_failure_title">Mailbox is unavailable</string>
<string name="mailbox_status_check_button">Check Connection</string>
<!-- Example for string substitution: Last connection: 3min ago-->
<string name="mailbox_status_connected_info">Last connection: %s</string>
<!-- Indicates that there never was a connection to the mailbox. Last connection: Never -->
<string name="mailbox_status_connected_never">Never</string>
<string name="mailbox_status_unlink_button">Unlink</string>
<!-- Conversation Settings -->
<string name="disappearing_messages_title">Disappearing messages</string>