diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java index dabfb9ea4..4276ccbdb 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java @@ -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(); + } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java index 7bbeb0e12..39b4d7fa0 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java @@ -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; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java index df58ace2d..8c5f16b32 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxActivity.java @@ -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); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java index 7d2ef097e..4fcfdaf6a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxState.java @@ -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; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxStatusFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxStatusFragment.java index 378354d0e..45f3456db 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxStatusFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxStatusFragment.java @@ -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); + } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java index 2a8e15a6b..3a47e1393 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/MailboxViewModel.java @@ -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 { + implements QrCodeDecoder.ResultCallback, Consumer, + 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 state = + private final MutableLiveEvent pairingState = new MutableLiveEvent<>(); + private final MutableLiveData 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 getState() { - return state; + void checkIfOnlineWhenPaired() { + boolean isOnline = isTorActive(); + pairingState.setEvent(new MailboxState.IsPaired(isOnline)); + } + + LiveData checkConnection() { + MutableLiveData 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 getPairingState() { + return pairingState; + } + + @UiThread + LiveData getStatus() { + return status; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java index 99ae679be..3ae1f6c18 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineFragment.java @@ -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(); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineStatusFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineStatusFragment.java new file mode 100644 index 000000000..ce2af02f8 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/mailbox/OfflineStatusFragment.java @@ -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(); + } + +} diff --git a/briar-android/src/main/res/layout/fragment_mailbox_status.xml b/briar-android/src/main/res/layout/fragment_mailbox_status.xml index 608edd3ba..58096f487 100644 --- a/briar-android/src/main/res/layout/fragment_mailbox_status.xml +++ b/briar-android/src/main/res/layout/fragment_mailbox_status.xml @@ -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" /> + +