Move Plugin related code from activity to ViewModel

This commit is contained in:
Torsten Grote
2021-02-02 17:47:47 -03:00
parent bed87ed439
commit 5a55b3d7e3
4 changed files with 280 additions and 238 deletions

View File

@@ -21,6 +21,11 @@ import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity; import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.WriteBlogPostActivity; import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.contact.ContactListFragment; import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeActivity;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeErrorFragment;
import org.briarproject.briar.android.contact.add.nearby.IntroFragment;
import org.briarproject.briar.android.contact.add.nearby.KeyAgreementActivity;
import org.briarproject.briar.android.contact.add.nearby.KeyAgreementFragment;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity; import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment; import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
import org.briarproject.briar.android.contact.add.remote.NicknameFragment; import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
@@ -36,10 +41,6 @@ import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment; import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeActivity;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeErrorFragment;
import org.briarproject.briar.android.contact.add.nearby.KeyAgreementActivity;
import org.briarproject.briar.android.contact.add.nearby.KeyAgreementFragment;
import org.briarproject.briar.android.login.ChangePasswordActivity; import org.briarproject.briar.android.login.ChangePasswordActivity;
import org.briarproject.briar.android.login.OpenDatabaseFragment; import org.briarproject.briar.android.login.OpenDatabaseFragment;
import org.briarproject.briar.android.login.PasswordFragment; import org.briarproject.briar.android.login.PasswordFragment;
@@ -208,6 +209,8 @@ public interface ActivityComponent {
void inject(FeedFragment fragment); void inject(FeedFragment fragment);
void inject(IntroFragment fragment);
void inject(KeyAgreementFragment fragment); void inject(KeyAgreementFragment fragment);
void inject(LinkExchangeFragment fragment); void inject(LinkExchangeFragment fragment);

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.app.Application; import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.widget.Toast; import android.widget.Toast;
@@ -30,8 +31,14 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent; import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeFinished; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeFinished;
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeStarted; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.ContactExchangeStarted;
@@ -40,6 +47,8 @@ import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyA
import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementWaiting; import org.briarproject.briar.android.contact.add.nearby.ContactAddingState.KeyAgreementWaiting;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Error; import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Error;
import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Success; import org.briarproject.briar.android.contact.add.nearby.ContactExchangeResult.Success;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@@ -55,12 +64,19 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.REFUSED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.UNKNOWN;
@NotNullByDefault @NotNullByDefault
class ContactExchangeViewModel extends AndroidViewModel class ContactExchangeViewModel extends AndroidViewModel
@@ -69,22 +85,80 @@ class ContactExchangeViewModel extends AndroidViewModel
private static final Logger LOG = private static final Logger LOG =
getLogger(ContactExchangeViewModel.class.getName()); getLogger(ContactExchangeViewModel.class.getName());
enum BluetoothDecision {
/**
* We haven't asked the user about Bluetooth discoverability.
*/
UNKNOWN,
/**
* The device doesn't have a Bluetooth adapter.
*/
NO_ADAPTER,
/**
* We're waiting for the user to accept or refuse discoverability.
*/
WAITING,
/**
* The user has accepted discoverability.
*/
ACCEPTED,
/**
* The user has refused discoverability.
*/
REFUSED
}
@SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19
private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
private final EventBus eventBus; private final EventBus eventBus;
private final Executor ioExecutor; private final Executor ioExecutor;
private final PluginManager pluginManager;
private final PayloadEncoder payloadEncoder; private final PayloadEncoder payloadEncoder;
private final PayloadParser payloadParser; private final PayloadParser payloadParser;
private final Provider<KeyAgreementTask> keyAgreementTaskProvider; private final Provider<KeyAgreementTask> keyAgreementTaskProvider;
private final ContactExchangeManager contactExchangeManager; private final ContactExchangeManager contactExchangeManager;
private final ConnectionManager connectionManager; private final ConnectionManager connectionManager;
/**
* Set to true when the continue button is clicked, and false when the QR
* code fragment is shown. This prevents the QR code fragment from being
* shown automatically before the continue button has been clicked.
*/
private final MutableLiveData<Boolean> wasContinueClicked =
new MutableLiveData<>(false);
private final MutableLiveEvent<Boolean> showQrCodeFragment =
new MutableLiveEvent<>();
private final MutableLiveEvent<TransportId> transportStateChanged =
new MutableLiveEvent<>();
private final MutableLiveData<ContactAddingState> state = private final MutableLiveData<ContactAddingState> state =
new MutableLiveData<>(); new MutableLiveData<>();
final QrCodeDecoder qrCodeDecoder; final QrCodeDecoder qrCodeDecoder;
@Nullable
private final BluetoothAdapter bt;
@Nullable
private final Plugin wifiPlugin, bluetoothPlugin;
// UiThread
BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
/**
* Records whether we've enabled the wifi plugin so we don't enable it more
* than once.
*/
private boolean hasEnabledWifi = false;
/**
* Records whether we've enabled the Bluetooth plugin so we don't enable it
* more than once.
*/
private boolean hasEnabledBluetooth = false;
@Nullable @Nullable
private KeyAgreementTask task; private KeyAgreementTask task;
private volatile boolean gotLocalPayload = false, gotRemotePayload = false; private volatile boolean gotLocalPayload = false, gotRemotePayload = false;
@@ -93,6 +167,7 @@ class ContactExchangeViewModel extends AndroidViewModel
ContactExchangeViewModel(Application app, ContactExchangeViewModel(Application app,
EventBus eventBus, EventBus eventBus,
@IoExecutor Executor ioExecutor, @IoExecutor Executor ioExecutor,
PluginManager pluginManager,
PayloadEncoder payloadEncoder, PayloadEncoder payloadEncoder,
PayloadParser payloadParser, PayloadParser payloadParser,
Provider<KeyAgreementTask> keyAgreementTaskProvider, Provider<KeyAgreementTask> keyAgreementTaskProvider,
@@ -101,11 +176,15 @@ class ContactExchangeViewModel extends AndroidViewModel
super(app); super(app);
this.eventBus = eventBus; this.eventBus = eventBus;
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.pluginManager = pluginManager;
this.payloadEncoder = payloadEncoder; this.payloadEncoder = payloadEncoder;
this.payloadParser = payloadParser; this.payloadParser = payloadParser;
this.keyAgreementTaskProvider = keyAgreementTaskProvider; this.keyAgreementTaskProvider = keyAgreementTaskProvider;
this.contactExchangeManager = contactExchangeManager; this.contactExchangeManager = contactExchangeManager;
this.connectionManager = connectionManager; this.connectionManager = connectionManager;
bt = BluetoothAdapter.getDefaultAdapter();
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
qrCodeDecoder = new QrCodeDecoder(ioExecutor, this); qrCodeDecoder = new QrCodeDecoder(ioExecutor, this);
eventBus.addListener(this); eventBus.addListener(this);
} }
@@ -117,12 +196,95 @@ class ContactExchangeViewModel extends AndroidViewModel
stopListening(); stopListening();
} }
@UiThread
void onContinueClicked() {
if (bluetoothDecision == REFUSED) {
bluetoothDecision = UNKNOWN; // Ask again
}
wasContinueClicked.setValue(true);
}
@UiThread
boolean isBluetoothSupported() {
return bt != null && bluetoothPlugin != null;
}
@UiThread
boolean isWifiReady() {
if (wifiPlugin == null) return true; // Continue without wifi
State state = wifiPlugin.getState();
// Wait for plugin to become enabled
return state == ACTIVE || state == INACTIVE;
}
@UiThread
boolean isBluetoothReady() {
if (bt == null || bluetoothPlugin == null) {
// Continue without Bluetooth
return true;
}
if (bluetoothDecision == BluetoothDecision.UNKNOWN ||
bluetoothDecision == BluetoothDecision.WAITING ||
bluetoothDecision == BluetoothDecision.REFUSED) {
// Wait for user to accept
return false;
}
if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// Wait for adapter to become discoverable
return false;
}
// Wait for plugin to become active
return bluetoothPlugin.getState() == ACTIVE;
}
@UiThread
void enableWifiIfWeShould() {
if (hasEnabledWifi) return;
if (wifiPlugin == null) return;
State state = wifiPlugin.getState();
if (state == STARTING_STOPPING || state == DISABLED) {
LOG.info("Enabling wifi plugin");
hasEnabledWifi = true;
pluginManager.setPluginEnabled(LanTcpConstants.ID, true);
}
}
@UiThread
void enableBluetoothIfWeShould() {
if (bluetoothDecision != BluetoothDecision.ACCEPTED) return;
if (hasEnabledBluetooth) return;
if (bluetoothPlugin == null || !isBluetoothSupported()) return;
State state = bluetoothPlugin.getState();
if (state == STARTING_STOPPING || state == DISABLED) {
LOG.info("Enabling Bluetooth plugin");
hasEnabledBluetooth = true;
pluginManager.setPluginEnabled(BluetoothConstants.ID, true);
}
}
@UiThread
void startAddingContact() {
// If we return to the intro fragment, the continue button needs to be
// clicked again before showing the QR code fragment
wasContinueClicked.setValue(false);
// If we return to the intro fragment, ask for Bluetooth
// discoverability again before showing the QR code fragment
bluetoothDecision = UNKNOWN;
// If we return to the intro fragment, we may need to enable wifi and
// Bluetooth again
hasEnabledWifi = false;
hasEnabledBluetooth = false;
// start to listen with a KeyAgreementTask
startListening();
showQrCodeFragment.setEvent(true);
}
/** /**
* Call this once Bluetooth and Wi-Fi are ready to be used. * Call this once Bluetooth and Wi-Fi are ready to be used.
* It is possible to call this more than once over the ViewModel's lifetime. * It is possible to call this more than once over the ViewModel's lifetime.
*/ */
@UiThread @UiThread
void startListening() { private void startListening() {
KeyAgreementTask oldTask = task; KeyAgreementTask oldTask = task;
KeyAgreementTask newTask = keyAgreementTaskProvider.get(); KeyAgreementTask newTask = keyAgreementTaskProvider.get();
task = newTask; task = newTask;
@@ -142,7 +304,20 @@ class ContactExchangeViewModel extends AndroidViewModel
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof KeyAgreementListeningEvent) { if (e instanceof TransportStateEvent) {
TransportStateEvent t = (TransportStateEvent) e;
if (t.getTransportId().equals(BluetoothConstants.ID)) {
if (LOG.isLoggable(INFO)) {
LOG.info("Bluetooth state changed to " + t.getState());
}
transportStateChanged.setEvent(t.getTransportId());
} else if (t.getTransportId().equals(LanTcpConstants.ID)) {
if (LOG.isLoggable(INFO)) {
LOG.info("Wifi state changed to " + t.getState());
}
transportStateChanged.setEvent(t.getTransportId());
}
} else if (e instanceof KeyAgreementListeningEvent) {
LOG.info("KeyAgreementListeningEvent received"); LOG.info("KeyAgreementListeningEvent received");
KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e; KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
onLocalPayloadReceived(event.getLocalPayload()); onLocalPayloadReceived(event.getLocalPayload());
@@ -257,6 +432,22 @@ class ContactExchangeViewModel extends AndroidViewModel
} }
} }
LiveData<Boolean> getWasContinueClicked() {
return wasContinueClicked;
}
/**
* Receives an event when the transport state of the WiFi or Bluetooth
* plugins changes.
*/
LiveEvent<TransportId> getTransportStateChanged() {
return transportStateChanged;
}
LiveEvent<Boolean> getShowQrCodeFragment() {
return showQrCodeFragment;
}
LiveData<ContactAddingState> getState() { LiveData<ContactAddingState> getState() {
return state; return state;
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -10,9 +9,13 @@ import android.widget.ScrollView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN; import static android.view.View.FOCUS_DOWN;
@@ -20,33 +23,27 @@ import static android.view.View.FOCUS_DOWN;
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class IntroFragment extends BaseFragment { public class IntroFragment extends BaseFragment {
interface IntroScreenSeenListener {
void showNextScreen();
}
public static final String TAG = IntroFragment.class.getName(); public static final String TAG = IntroFragment.class.getName();
private IntroScreenSeenListener screenSeenListener; @Inject
ViewModelProvider.Factory viewModelFactory;
private ContactExchangeViewModel viewModel;
private ScrollView scrollView; private ScrollView scrollView;
public static IntroFragment newInstance() { public static IntroFragment newInstance() {
Bundle args = new Bundle(); Bundle args = new Bundle();
IntroFragment fragment = new IntroFragment(); IntroFragment fragment = new IntroFragment();
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public void onAttach(Context context) { public void injectFragment(ActivityComponent component) {
super.onAttach(context); component.inject(this);
screenSeenListener = (IntroScreenSeenListener) context; viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
} .get(ContactExchangeViewModel.class);
@Override
public String getUniqueTag() {
return TAG;
} }
@Nullable @Nullable
@@ -59,7 +56,7 @@ public class IntroFragment extends BaseFragment {
false); false);
scrollView = v.findViewById(R.id.scrollView); scrollView = v.findViewById(R.id.scrollView);
View button = v.findViewById(R.id.continueButton); View button = v.findViewById(R.id.continueButton);
button.setOnClickListener(view -> screenSeenListener.showNextScreen()); button.setOnClickListener(view -> viewModel.onContinueClicked());
return v; return v;
} }
@@ -69,4 +66,9 @@ public class IntroFragment extends BaseFragment {
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
} }
@Override
public String getUniqueTag() {
return TAG;
}
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.contact.add.nearby;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -8,21 +7,12 @@ import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
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.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.Plugin;
import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.contact.add.nearby.IntroFragment.IntroScreenSeenListener; import org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision;
import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
@@ -43,51 +33,21 @@ import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.CAMERA;
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.ACCEPTED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.REFUSED;
import static org.briarproject.briar.android.contact.add.nearby.ContactExchangeViewModel.BluetoothDecision.UNKNOWN;
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public abstract class KeyAgreementActivity extends BriarActivity implements public abstract class KeyAgreementActivity extends BriarActivity
BaseFragmentListener, IntroScreenSeenListener, EventListener { implements BaseFragmentListener {
private enum BluetoothDecision {
/**
* We haven't asked the user about Bluetooth discoverability.
*/
UNKNOWN,
/**
* The device doesn't have a Bluetooth adapter.
*/
NO_ADAPTER,
/**
* We're waiting for the user to accept or refuse discoverability.
*/
WAITING,
/**
* The user has accepted discoverability.
*/
ACCEPTED,
/**
* The user has refused discoverability.
*/
REFUSED
}
private enum Permission { private enum Permission {
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
@@ -96,16 +56,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private static final Logger LOG = private static final Logger LOG =
getLogger(KeyAgreementActivity.class.getName()); getLogger(KeyAgreementActivity.class.getName());
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@Inject
EventBus eventBus;
@Inject
PluginManager pluginManager;
protected ContactExchangeViewModel viewModel; protected ContactExchangeViewModel viewModel;
/** /**
@@ -116,31 +69,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
*/ */
private boolean isResumed = false; private boolean isResumed = false;
/**
* Set to true when the continue button is clicked, and false when the QR
* code fragment is shown. This prevents the QR code fragment from being
* shown automatically before the continue button has been clicked.
*/
private boolean continueClicked = false;
/**
* Records whether we've enabled the wifi plugin so we don't enable it more
* than once.
*/
private boolean hasEnabledWifi = false;
/**
* Records whether we've enabled the Bluetooth plugin so we don't enable it
* more than once.
*/
private boolean hasEnabledBluetooth = false;
private Permission cameraPermission = Permission.UNKNOWN; private Permission cameraPermission = Permission.UNKNOWN;
private Permission locationPermission = Permission.UNKNOWN; private Permission locationPermission = Permission.UNKNOWN;
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
private BroadcastReceiver bluetoothReceiver = null; private BroadcastReceiver bluetoothReceiver = null;
private Plugin wifiPlugin = null, bluetoothPlugin = null;
private BluetoothAdapter bt = null;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -162,9 +93,42 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
bluetoothReceiver = new BluetoothStateReceiver(); bluetoothReceiver = new BluetoothStateReceiver();
registerReceiver(bluetoothReceiver, filter); registerReceiver(bluetoothReceiver, filter);
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID); viewModel.getWasContinueClicked().observe(this, clicked -> {
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); if (clicked && checkPermissions()) showQrCodeFragmentIfAllowed();
bt = BluetoothAdapter.getDefaultAdapter(); });
viewModel.getTransportStateChanged().observeEvent(this,
t -> showQrCodeFragmentIfAllowed());
viewModel.getShowQrCodeFragment().observeEvent(this, show -> {
if (show) showQrCodeFragment();
});
}
@Override
public void onStart() {
super.onStart();
// Permissions may have been granted manually while we were stopped
cameraPermission = Permission.UNKNOWN;
locationPermission = Permission.UNKNOWN;
}
@Override
protected void onPostResume() {
super.onPostResume();
isResumed = true;
// Workaround for
// https://code.google.com/p/android/issues/detail?id=190966
showQrCodeFragmentIfAllowed();
}
@Override
protected void onPause() {
super.onPause();
isResumed = false;
}
@Override
protected void onStop() {
super.onStop();
} }
@Override @Override
@@ -182,45 +146,22 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onStart() {
super.onStart();
eventBus.addListener(this);
// Permissions may have been granted manually while we were stopped
cameraPermission = Permission.UNKNOWN;
locationPermission = Permission.UNKNOWN;
}
@Override
protected void onPostResume() {
super.onPostResume();
isResumed = true;
// Workaround for
// https://code.google.com/p/android/issues/detail?id=190966
showQrCodeFragmentIfAllowed();
}
@SuppressWarnings("StatementWithEmptyBody") @SuppressWarnings("StatementWithEmptyBody")
private void showQrCodeFragmentIfAllowed() { private void showQrCodeFragmentIfAllowed() {
boolean continueClicked = // never set to null
requireNonNull(viewModel.getWasContinueClicked().getValue());
if (isResumed && continueClicked && areEssentialPermissionsGranted()) { if (isResumed && continueClicked && areEssentialPermissionsGranted()) {
if (isWifiReady() && isBluetoothReady()) { if (viewModel.isWifiReady() && viewModel.isBluetoothReady()) {
LOG.info("Wifi and Bluetooth are ready"); LOG.info("Wifi and Bluetooth are ready");
viewModel.startListening(); viewModel.startAddingContact();
showQrCodeFragment();
} else { } else {
if (shouldEnableWifi()) { viewModel.enableWifiIfWeShould();
LOG.info("Enabling wifi plugin"); if (viewModel.bluetoothDecision == UNKNOWN) {
hasEnabledWifi = true;
pluginManager.setPluginEnabled(LanTcpConstants.ID, true);
}
if (bluetoothDecision == BluetoothDecision.UNKNOWN) {
requestBluetoothDiscoverable(); requestBluetoothDiscoverable();
} else if (bluetoothDecision == BluetoothDecision.REFUSED) { } else if (viewModel.bluetoothDecision == REFUSED) {
// Ask again when the user clicks "continue" // Ask again when the user clicks "continue"
} else if (shouldEnableBluetooth()) { } else {
LOG.info("Enabling Bluetooth plugin"); viewModel.enableBluetoothIfWeShould();
hasEnabledBluetooth = true;
pluginManager.setPluginEnabled(BluetoothConstants.ID, true);
} }
} }
} }
@@ -229,119 +170,42 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private boolean areEssentialPermissionsGranted() { private boolean areEssentialPermissionsGranted() {
return cameraPermission == Permission.GRANTED && return cameraPermission == Permission.GRANTED &&
(SDK_INT < 23 || locationPermission == Permission.GRANTED || (SDK_INT < 23 || locationPermission == Permission.GRANTED ||
!isBluetoothSupported()); !viewModel.isBluetoothSupported());
}
private boolean isBluetoothSupported() {
return bt != null && bluetoothPlugin != null;
}
private boolean isWifiReady() {
if (wifiPlugin == null) return true; // Continue without wifi
State state = wifiPlugin.getState();
// Wait for plugin to become enabled
return state == ACTIVE || state == INACTIVE;
}
private boolean isBluetoothReady() {
if (!isBluetoothSupported()) {
// Continue without Bluetooth
return true;
}
if (bluetoothDecision == BluetoothDecision.UNKNOWN ||
bluetoothDecision == BluetoothDecision.WAITING ||
bluetoothDecision == BluetoothDecision.REFUSED) {
// Wait for user to accept
return false;
}
if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// Wait for adapter to become discoverable
return false;
}
// Wait for plugin to become active
return bluetoothPlugin.getState() == ACTIVE;
}
private boolean shouldEnableWifi() {
if (hasEnabledWifi) return false;
if (wifiPlugin == null) return false;
State state = wifiPlugin.getState();
return state == STARTING_STOPPING || state == DISABLED;
} }
private void requestBluetoothDiscoverable() { private void requestBluetoothDiscoverable() {
if (!isBluetoothSupported()) { if (!viewModel.isBluetoothSupported()) {
bluetoothDecision = BluetoothDecision.NO_ADAPTER; viewModel.bluetoothDecision = BluetoothDecision.NO_ADAPTER;
showQrCodeFragmentIfAllowed(); showQrCodeFragmentIfAllowed();
} else { } else {
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
if (i.resolveActivity(getPackageManager()) != null) { if (i.resolveActivity(getPackageManager()) != null) {
LOG.info("Asking for Bluetooth discoverability"); LOG.info("Asking for Bluetooth discoverability");
bluetoothDecision = BluetoothDecision.WAITING; viewModel.bluetoothDecision = BluetoothDecision.WAITING;
startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE); startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
} else { } else {
bluetoothDecision = BluetoothDecision.NO_ADAPTER; viewModel.bluetoothDecision = BluetoothDecision.NO_ADAPTER;
showQrCodeFragmentIfAllowed(); showQrCodeFragmentIfAllowed();
} }
} }
} }
private boolean shouldEnableBluetooth() {
if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false;
if (hasEnabledBluetooth) return false;
if (!isBluetoothSupported()) return false;
State state = bluetoothPlugin.getState();
return state == STARTING_STOPPING || state == DISABLED;
}
@Override
protected void onPause() {
super.onPause();
isResumed = false;
}
@Override
protected void onStop() {
super.onStop();
eventBus.removeListener(this);
}
@Override
public void showNextScreen() {
continueClicked = true;
if (bluetoothDecision == BluetoothDecision.REFUSED) {
bluetoothDecision = BluetoothDecision.UNKNOWN; // Ask again
}
if (checkPermissions()) showQrCodeFragmentIfAllowed();
}
@Override @Override
public void onActivityResult(int request, int result, public void onActivityResult(int request, int result,
@Nullable Intent data) { @Nullable Intent data) {
if (request == REQUEST_BLUETOOTH_DISCOVERABLE) { if (request == REQUEST_BLUETOOTH_DISCOVERABLE) {
if (result == RESULT_CANCELED) { if (result == RESULT_CANCELED) {
LOG.info("Bluetooth discoverability was refused"); LOG.info("Bluetooth discoverability was refused");
bluetoothDecision = BluetoothDecision.REFUSED; viewModel.bluetoothDecision = REFUSED;
} else { } else {
LOG.info("Bluetooth discoverability was accepted"); LOG.info("Bluetooth discoverability was accepted");
bluetoothDecision = BluetoothDecision.ACCEPTED; viewModel.bluetoothDecision = ACCEPTED;
} }
showQrCodeFragmentIfAllowed(); showQrCodeFragmentIfAllowed();
} else super.onActivityResult(request, result, data); } else super.onActivityResult(request, result, data);
} }
private void showQrCodeFragment() { private void showQrCodeFragment() {
// If we return to the intro fragment, the continue button needs to be
// clicked again before showing the QR code fragment
continueClicked = false;
// If we return to the intro fragment, ask for Bluetooth
// discoverability again before showing the QR code fragment
bluetoothDecision = BluetoothDecision.UNKNOWN;
// If we return to the intro fragment, we may need to enable wifi and
// Bluetooth again
hasEnabledWifi = false;
hasEnabledBluetooth = false;
// FIXME #824 // FIXME #824
FragmentManager fm = getSupportFragmentManager(); FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) { if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
@@ -362,7 +226,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
R.string.permission_camera_denied_body); R.string.permission_camera_denied_body);
return false; return false;
} }
if (isBluetoothSupported() && if (viewModel.isBluetoothSupported() &&
locationPermission == Permission.PERMANENTLY_DENIED) { locationPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(R.string.permission_location_title, showDenialDialog(R.string.permission_location_title,
R.string.permission_location_denied_body); R.string.permission_location_denied_body);
@@ -406,7 +270,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
private void requestPermissions() { private void requestPermissions() {
String[] permissions; String[] permissions;
if (isBluetoothSupported()) { if (viewModel.isBluetoothSupported()) {
permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION}; permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
} else { } else {
permissions = new String[] {CAMERA}; permissions = new String[] {CAMERA};
@@ -430,7 +294,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
} else { } else {
cameraPermission = Permission.PERMANENTLY_DENIED; cameraPermission = Permission.PERMANENTLY_DENIED;
} }
if (isBluetoothSupported()) { if (viewModel.isBluetoothSupported()) {
if (gotPermission(ACCESS_FINE_LOCATION, permissions, if (gotPermission(ACCESS_FINE_LOCATION, permissions,
grantResults)) { grantResults)) {
locationPermission = Permission.GRANTED; locationPermission = Permission.GRANTED;
@@ -463,24 +327,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
permission); permission);
} }
@Override
public void eventOccurred(Event e) {
if (e instanceof TransportStateEvent) {
TransportStateEvent t = (TransportStateEvent) e;
if (t.getTransportId().equals(BluetoothConstants.ID)) {
if (LOG.isLoggable(INFO)) {
LOG.info("Bluetooth state changed to " + t.getState());
}
showQrCodeFragmentIfAllowed();
} else if (t.getTransportId().equals(LanTcpConstants.ID)) {
if (LOG.isLoggable(INFO)) {
LOG.info("Wifi state changed to " + t.getState());
}
showQrCodeFragmentIfAllowed();
}
}
}
private class BluetoothStateReceiver extends BroadcastReceiver { private class BluetoothStateReceiver extends BroadcastReceiver {
@Override @Override