diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java index 856103e18..ef03a78fd 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java @@ -53,6 +53,7 @@ import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; 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.STARTING_STOPPING; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; @@ -354,6 +355,10 @@ class PluginManagerImpl implements PluginManager, Service { } else if (oldState == ACTIVE) { eventBus.broadcast(new TransportInactiveEvent(id)); } + } else if (newState == DISABLED) { + // Broadcast an event even though the state hasn't changed, as + // the reasons for the plugin being disabled may have changed + eventBus.broadcast(new TransportStateEvent(id, newState)); } } diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 5055324bf..a54a99b34 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -1,46 +1,46 @@ - + + android:required="false" /> + android:required="false" /> - + android:required="false" /> + - - - - - - - + + + + + + + - - - - + + + + - - - - + + + + - - + + @@ -59,14 +59,13 @@ android:name="org.briarproject.briar.android.BriarService" android:exported="false"> - + - + android:exported="false"> - + android:windowSoftInputMode="adjustResize|stateHidden"> - + android:label="@string/app_name"> - + android:label="@string/app_name"> - + android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> - - + + + @@ -111,17 +107,17 @@ android:launchMode="singleTask" android:theme="@style/BriarTheme.NoActionBar"> - + - - + + - + - - - + + + @@ -133,7 +129,7 @@ android:windowSoftInputMode="adjustResize|stateUnchanged"> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.conversation.ConversationActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity" /> + android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity" /> + android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.conversation.ConversationActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.forum.ForumActivity" /> + android:value="org.briarproject.briar.android.blog.BlogActivity" /> + android:value="org.briarproject.briar.android.forum.ForumActivity" /> + android:value="org.briarproject.briar.android.blog.BlogActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.blog.BlogActivity" /> + android:value="org.briarproject.briar.android.blog.BlogActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> + android:value="org.briarproject.briar.android.conversation.ConversationActivity" /> - + android:label="@string/startup_failed_activity_title"> + android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity" /> - - + + + + + + + android:value="org.briarproject.briar.android.settings.SettingsActivity" /> + android:value="org.briarproject.briar.android.settings.SettingsActivity" /> + android:value="org.briarproject.briar.android.settings.SettingsActivity" /> - - + + - + android:theme="@android:style/Theme.NoDisplay"> - + android:theme="@android:style/Theme.NoDisplay"> + android:theme="@style/BriarTheme.NoActionBar" /> + android:windowSoftInputMode="adjustResize|stateHidden" /> + android:theme="@style/BriarTheme" /> diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 02c18b341..e71e7d786 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -47,6 +47,7 @@ import org.briarproject.briar.android.login.OpenDatabaseFragment; import org.briarproject.briar.android.login.PasswordFragment; import org.briarproject.briar.android.login.StartupActivity; import org.briarproject.briar.android.navdrawer.NavDrawerActivity; +import org.briarproject.briar.android.navdrawer.TransportsActivity; import org.briarproject.briar.android.panic.PanicPreferencesActivity; import org.briarproject.briar.android.panic.PanicResponderActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity; @@ -163,6 +164,8 @@ public interface ActivityComponent { void inject(SettingsActivity activity); + void inject(TransportsActivity activity); + void inject(TestDataActivity activity); void inject(ChangePasswordActivity activity); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java index 5376dc8f0..3d89bb421 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java @@ -11,6 +11,7 @@ import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import com.google.android.material.navigation.NavigationView; @@ -56,8 +57,10 @@ import androidx.core.content.ContextCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; +import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -75,6 +78,8 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.navdrawer.IntentRouter.handleExternalIntent; import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry; +import static org.briarproject.briar.android.util.UiUtils.observeOnce; +import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -97,6 +102,9 @@ public class NavDrawerActivity extends BriarActivity implements public static Uri SIGN_OUT_URI = Uri.parse("briar-content://org.briarproject.briar/sign-out"); + private final List transports = new ArrayList<>(3); + private final MutableLiveData torIcon = new MutableLiveData<>(); + private NavDrawerViewModel navDrawerViewModel; private PluginViewModel pluginViewModel; private ActionBarDrawerToggle drawerToggle; @@ -110,7 +118,6 @@ public class NavDrawerActivity extends BriarActivity implements private DrawerLayout drawerLayout; private NavigationView navigation; - private List transports; private BaseAdapter transportsAdapter; @Override @@ -141,6 +148,11 @@ public class NavDrawerActivity extends BriarActivity implements drawerLayout = findViewById(R.id.drawer_layout); navigation = findViewById(R.id.navigation); GridView transportsView = findViewById(R.id.transportsView); + LinearLayout transportsLayout = findViewById(R.id.transports); + transportsLayout.setOnClickListener(v -> { + LOG.info("Starting transports activity"); + startActivity(new Intent(this, TransportsActivity.class)); + }); setSupportActionBar(toolbar); ActionBar actionBar = requireNonNull(getSupportActionBar()); @@ -149,13 +161,23 @@ public class NavDrawerActivity extends BriarActivity implements drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.nav_drawer_open_description, - R.string.nav_drawer_close_description); + R.string.nav_drawer_close_description) { + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + navDrawerViewModel.checkTransportsOnboarding(); + } + }; drawerLayout.addDrawerListener(drawerToggle); navigation.setNavigationItemSelectedListener(this); initializeTransports(); transportsView.setAdapter(transportsAdapter); + observeOnce(navDrawerViewModel.showTransportsOnboarding(), this, show -> + observeOnce(torIcon, this, imageView -> + showTransportsOnboarding(show, imageView))); + lockManager.isLockable().observe(this, this::setLockVisible); if (lifecycleManager.getLifecycleState().isAfter(RUNNING)) { @@ -380,8 +402,6 @@ public class NavDrawerActivity extends BriarActivity implements } private void initializeTransports() { - transports = new ArrayList<>(3); - transportsAdapter = new BaseAdapter() { @Override @@ -422,6 +442,8 @@ public class NavDrawerActivity extends BriarActivity implements TextView text = view.findViewById(R.id.textView); text.setText(getString(t.label)); + if (t.id.equals(TorConstants.ID)) torIcon.setValue(icon); + return view; } }; @@ -444,7 +466,7 @@ public class NavDrawerActivity extends BriarActivity implements private Transport createTransport(TransportId id, @DrawableRes int iconDrawable, @StringRes int label) { int iconColor = getIconColor(STARTING_STOPPING); - Transport transport = new Transport(iconDrawable, label, iconColor); + Transport transport = new Transport(id, iconDrawable, label, iconColor); pluginViewModel.getPluginState(id).observe(this, state -> { transport.iconColor = getIconColor(state); transportsAdapter.notifyDataSetChanged(); @@ -452,8 +474,25 @@ public class NavDrawerActivity extends BriarActivity implements return transport; } + private void showTransportsOnboarding(boolean show, ImageView imageView) { + if (show) { + int color = resolveColorAttribute(this, R.attr.colorControlNormal); + new MaterialTapTargetPrompt.Builder(NavDrawerActivity.this, + R.style.OnboardingDialogTheme).setTarget(imageView) + .setPrimaryText(R.string.network_settings_title) + .setSecondaryText(R.string.transports_onboarding_text) + .setIcon(R.drawable.transport_tor) + .setIconDrawableColourFilter(color) + .setBackgroundColour( + ContextCompat.getColor(this, R.color.briar_primary)) + .show(); + navDrawerViewModel.transportsOnboardingShown(); + } + } + private static class Transport { + private final TransportId id; @DrawableRes private final int iconDrawable; @StringRes @@ -462,8 +501,9 @@ public class NavDrawerActivity extends BriarActivity implements @ColorRes private int iconColor; - private Transport(@DrawableRes int iconDrawable, @StringRes int label, - @ColorRes int iconColor) { + private Transport(TransportId id, @DrawableRes int iconDrawable, + @StringRes int label, @ColorRes int iconColor) { + this.id = id; this.iconDrawable = iconDrawable; this.label = label; this.iconColor = iconColor; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java index 1d031d35d..c354e0f66 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java @@ -34,6 +34,8 @@ public class NavDrawerViewModel extends AndroidViewModel { getLogger(NavDrawerViewModel.class.getName()); private static final String EXPIRY_DATE_WARNING = "expiryDateWarning"; + private static final String SHOW_TRANSPORTS_ONBOARDING = + "showTransportsOnboarding"; @DatabaseExecutor private final Executor dbExecutor; @@ -43,6 +45,8 @@ public class NavDrawerViewModel extends AndroidViewModel { new MutableLiveData<>(); private final MutableLiveData shouldAskForDozeWhitelisting = new MutableLiveData<>(); + private final MutableLiveData showTransportsOnboarding = + new MutableLiveData<>(); @Inject NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor, @@ -128,4 +132,39 @@ public class NavDrawerViewModel extends AndroidViewModel { } }); } + + @UiThread + LiveData showTransportsOnboarding() { + return showTransportsOnboarding; + } + + @UiThread + void checkTransportsOnboarding() { + if (showTransportsOnboarding.getValue() != null) return; + dbExecutor.execute(() -> { + try { + Settings settings = + settingsManager.getSettings(SETTINGS_NAMESPACE); + boolean show = + settings.getBoolean(SHOW_TRANSPORTS_ONBOARDING, true); + showTransportsOnboarding.postValue(show); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + @UiThread + void transportsOnboardingShown() { + showTransportsOnboarding.setValue(false); + dbExecutor.execute(() -> { + try { + Settings settings = new Settings(); + settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false); + settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java index 8badce8c4..cc2c5ef7f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java @@ -1,8 +1,20 @@ package org.briarproject.briar.android.navdrawer; +import android.app.Application; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; 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.network.NetworkManager; +import org.briarproject.bramble.api.network.NetworkStatus; +import org.briarproject.bramble.api.network.event.NetworkStatusEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.LanTcpConstants; @@ -12,28 +24,44 @@ 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.TransportStateEvent; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.settings.SettingsManager; +import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; +import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.inject.Inject; import androidx.annotation.Nullable; +import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; +import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; +import static android.bluetooth.BluetoothAdapter.EXTRA_STATE; +import static android.bluetooth.BluetoothAdapter.STATE_ON; import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; +import static org.briarproject.bramble.util.LogUtils.logDuration; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.LogUtils.now; @NotNullByDefault -public class PluginViewModel extends ViewModel implements EventListener { +public class PluginViewModel extends AndroidViewModel implements EventListener { private static final Logger LOG = getLogger(PluginViewModel.class.getName()); + private final Application app; + private final Executor dbExecutor; + private final SettingsManager settingsManager; private final PluginManager pluginManager; private final EventBus eventBus; + private final BroadcastReceiver receiver; private final MutableLiveData torPluginState = new MutableLiveData<>(); @@ -42,24 +70,68 @@ public class PluginViewModel extends ViewModel implements EventListener { private final MutableLiveData btPluginState = new MutableLiveData<>(); + private final MutableLiveData torEnabledSetting = + new MutableLiveData<>(false); + private final MutableLiveData wifiEnabledSetting = + new MutableLiveData<>(false); + private final MutableLiveData btEnabledSetting = + new MutableLiveData<>(false); + + private final MutableLiveData networkStatus = + new MutableLiveData<>(); + + private final MutableLiveData bluetoothTurnedOn = + new MutableLiveData<>(false); + @Inject - PluginViewModel(PluginManager pluginManager, EventBus eventBus) { + PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor, + SettingsManager settingsManager, PluginManager pluginManager, + EventBus eventBus, NetworkManager networkManager) { + super(app); + this.app = app; + this.dbExecutor = dbExecutor; + this.settingsManager = settingsManager; this.pluginManager = pluginManager; this.eventBus = eventBus; eventBus.addListener(this); + receiver = new BluetoothStateReceiver(); + app.registerReceiver(receiver, new IntentFilter(ACTION_STATE_CHANGED)); + networkStatus.setValue(networkManager.getNetworkStatus()); torPluginState.setValue(getTransportState(TorConstants.ID)); wifiPluginState.setValue(getTransportState(LanTcpConstants.ID)); btPluginState.setValue(getTransportState(BluetoothConstants.ID)); + initialiseBluetoothState(); + loadSettings(); } @Override protected void onCleared() { eventBus.removeListener(this); + app.unregisterReceiver(receiver); } @Override public void eventOccurred(Event e) { - if (e instanceof TransportStateEvent) { + if (e instanceof NetworkStatusEvent) { + networkStatus.setValue(((NetworkStatusEvent) e).getStatus()); + } else if (e instanceof SettingsUpdatedEvent) { + SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; + if (s.getNamespace().equals(TorConstants.ID.getString())) { + boolean enable = s.getSettings().getBoolean(PREF_PLUGIN_ENABLE, + TorConstants.DEFAULT_PREF_PLUGIN_ENABLE); + torEnabledSetting.setValue(enable); + } else if (s.getNamespace().equals( + LanTcpConstants.ID.getString())) { + boolean enable = s.getSettings().getBoolean(PREF_PLUGIN_ENABLE, + LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE); + wifiEnabledSetting.setValue(enable); + } else if (s.getNamespace().equals( + BluetoothConstants.ID.getString())) { + boolean enable = s.getSettings().getBoolean(PREF_PLUGIN_ENABLE, + BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE); + btEnabledSetting.setValue(enable); + } + } else if (e instanceof TransportStateEvent) { TransportStateEvent t = (TransportStateEvent) e; TransportId id = t.getTransportId(); State state = t.getState(); @@ -77,6 +149,62 @@ public class PluginViewModel extends ViewModel implements EventListener { return liveData; } + LiveData getPluginEnabledSetting(TransportId id) { + if (id.equals(TorConstants.ID)) return torEnabledSetting; + else if (id.equals(LanTcpConstants.ID)) return wifiEnabledSetting; + else if (id.equals(BluetoothConstants.ID)) return btEnabledSetting; + else throw new IllegalArgumentException(); + } + + LiveData getNetworkStatus() { + return networkStatus; + } + + LiveData getBluetoothTurnedOn() { + return bluetoothTurnedOn; + } + + int getReasonsTorDisabled() { + Plugin plugin = pluginManager.getPlugin(TorConstants.ID); + return plugin == null ? 0 : plugin.getReasonsDisabled(); + } + + void enableTransport(TransportId id, boolean enable) { + Settings s = new Settings(); + s.putBoolean(PREF_PLUGIN_ENABLE, enable); + mergeSettings(s, id.getString()); + } + + private void initialiseBluetoothState() { + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) bluetoothTurnedOn.setValue(false); + else bluetoothTurnedOn.setValue(bt.getState() == STATE_ON); + } + + private void loadSettings() { + dbExecutor.execute(() -> { + try { + boolean tor = isPluginEnabled(TorConstants.ID, + TorConstants.DEFAULT_PREF_PLUGIN_ENABLE); + torEnabledSetting.postValue(tor); + boolean wifi = isPluginEnabled(LanTcpConstants.ID, + LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE); + wifiEnabledSetting.postValue(wifi); + boolean bt = isPluginEnabled(BluetoothConstants.ID, + BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE); + btEnabledSetting.postValue(bt); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private boolean isPluginEnabled(TransportId id, boolean defaultValue) + throws DbException { + Settings s = settingsManager.getSettings(id.getString()); + return s.getBoolean(PREF_PLUGIN_ENABLE, defaultValue); + } + private State getTransportState(TransportId id) { Plugin plugin = pluginManager.getPlugin(id); return plugin == null ? STARTING_STOPPING : plugin.getState(); @@ -89,4 +217,26 @@ public class PluginViewModel extends ViewModel implements EventListener { else if (id.equals(BluetoothConstants.ID)) return btPluginState; else return null; } + + private void mergeSettings(Settings s, String namespace) { + dbExecutor.execute(() -> { + try { + long start = now(); + settingsManager.mergeSettings(s, namespace); + logDuration(LOG, "Merging settings", start); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private class BluetoothStateReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + int state = intent.getIntExtra(EXTRA_STATE, 0); + if (state == STATE_ON) bluetoothTurnedOn.postValue(true); + else bluetoothTurnedOn.postValue(false); + } + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java new file mode 100644 index 000000000..688700cb9 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java @@ -0,0 +1,360 @@ +package org.briarproject.briar.android.navdrawer; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.TextView; + +import org.briarproject.bramble.api.network.NetworkStatus; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +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.State; +import org.briarproject.bramble.api.plugin.TorConstants; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.activity.BriarActivity; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelProviders; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +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.ENABLING; +import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA; +import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class TransportsActivity extends BriarActivity { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private final List transports = new ArrayList<>(3); + + private PluginViewModel viewModel; + private BaseAdapter transportsAdapter; + + @Override + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + + setContentView(R.layout.activity_transports); + + ViewModelProvider provider = + ViewModelProviders.of(this, viewModelFactory); + viewModel = provider.get(PluginViewModel.class); + + GridView grid = findViewById(R.id.grid); + initializeCards(); + grid.setAdapter(transportsAdapter); + } + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } else if (item.getItemId() == R.id.action_help) { + String text = getString(R.string.transports_help_text); + showOnboardingDialog(this, text); + return true; + } + return false; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.help_action, menu); + return super.onCreateOptionsMenu(menu); + } + + private void initializeCards() { + transportsAdapter = new BaseAdapter() { + + @Override + public int getCount() { + return transports.size(); + } + + @Override + public Transport getItem(int position) { + return transports.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, + ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; + } else { + LayoutInflater inflater = getLayoutInflater(); + view = inflater.inflate(R.layout.list_item_transport_card, + parent, false); + } + + Transport t = getItem(position); + + ImageView icon = view.findViewById(R.id.icon); + icon.setImageDrawable(ContextCompat.getDrawable( + TransportsActivity.this, t.iconDrawable)); + icon.setColorFilter(ContextCompat.getColor( + TransportsActivity.this, t.iconColor)); + + TextView title = view.findViewById(R.id.title); + title.setText(getString(t.title)); + + SwitchCompat switchCompat = + view.findViewById(R.id.switchCompat); + switchCompat.setText(getString(t.switchLabel)); + switchCompat.setOnClickListener(v -> + viewModel.enableTransport(t.id, + switchCompat.isChecked())); + switchCompat.setChecked(t.isSwitchChecked); + + TextView summary = view.findViewById(R.id.summary); + if (t.summary == 0) { + summary.setVisibility(GONE); + } else { + summary.setText(t.summary); + summary.setVisibility(VISIBLE); + } + + TextView deviceStatus = view.findViewById(R.id.deviceStatus); + deviceStatus.setText(getBulletString(t.deviceStatus)); + + TextView pluginStatus = view.findViewById(R.id.appStatus); + pluginStatus.setText(getBulletString(t.pluginStatus)); + pluginStatus.setVisibility(t.showPluginStatus ? VISIBLE : GONE); + + return view; + } + }; + + Transport tor = createTransport(TorConstants.ID, + R.drawable.transport_tor, R.string.transport_tor, + R.string.tor_enable_title, R.string.tor_enable_summary, + R.string.tor_device_status_offline, + R.string.tor_plugin_status_inactive); + transports.add(tor); + + Transport wifi = createTransport(LanTcpConstants.ID, + R.drawable.transport_lan, R.string.transport_lan_long, + R.string.wifi_setting, 0, R.string.lan_device_status_off, + R.string.lan_plugin_status_inactive); + transports.add(wifi); + + Transport bt = createTransport(BluetoothConstants.ID, + R.drawable.transport_bt, R.string.transport_bt, + R.string.bluetooth_setting, 0, R.string.bt_device_status_off, + R.string.bt_plugin_status_inactive); + transports.add(bt); + + viewModel.getNetworkStatus().observe(this, status -> { + updateTorResources(tor, status); + updateWifiResources(wifi, status); + transportsAdapter.notifyDataSetChanged(); + }); + + viewModel.getBluetoothTurnedOn().observe(this, on -> { + updateBtResources(bt, on); + transportsAdapter.notifyDataSetChanged(); + }); + } + + private String getBulletString(@StringRes int resId) { + return "\u2022 " + getString(resId); + } + + @ColorRes + private int getIconColor(State state) { + if (state == ACTIVE) return R.color.briar_lime_400; + else if (state == ENABLING) return R.color.briar_orange_500; + else return android.R.color.tertiary_text_light; + } + + private void updateTorResources(Transport tor, NetworkStatus status) { + if (status.isConnected()) { + if (status.isWifi()) { + tor.deviceStatus = R.string.tor_device_status_online_wifi; + } else { + tor.deviceStatus = R.string.tor_device_status_online_mobile; + } + tor.showPluginStatus = true; + } else { + tor.deviceStatus = R.string.tor_device_status_offline; + tor.showPluginStatus = false; + } + } + + private void updateWifiResources(Transport wifi, NetworkStatus status) { + if (status.isWifi()) { + wifi.deviceStatus = R.string.lan_device_status_on; + wifi.showPluginStatus = true; + } else { + wifi.deviceStatus = R.string.lan_device_status_off; + wifi.showPluginStatus = false; + } + } + + private void updateBtResources(Transport bt, boolean on) { + if (on) { + bt.deviceStatus = R.string.bt_device_status_on; + bt.showPluginStatus = true; + } else { + bt.deviceStatus = R.string.bt_device_status_off; + bt.showPluginStatus = false; + } + } + + @StringRes + private int getPluginStatus(TransportId id, State state) { + if (id.equals(TorConstants.ID)) { + return getTorPluginStatus(state); + } else if (id.equals(LanTcpConstants.ID)) { + return getWifiPluginStatus(state); + } else if (id.equals(BluetoothConstants.ID)) { + return getBtPluginStatus(state); + } else throw new AssertionError(); + } + + @StringRes + private int getTorPluginStatus(State state) { + if (state == ENABLING) { + return R.string.tor_plugin_status_enabling; + } else if (state == ACTIVE) { + return R.string.tor_plugin_status_active; + } else if (state == DISABLED) { + int reasons = viewModel.getReasonsTorDisabled(); + if ((reasons & REASON_MOBILE_DATA) != 0) { + return R.string.tor_plugin_status_disabled_mobile_data; + } else if ((reasons & REASON_BATTERY) != 0) { + return R.string.tor_plugin_status_disabled_battery; + } else if ((reasons & REASON_COUNTRY_BLOCKED) != 0) { + return R.string.tor_plugin_status_disabled_country_blocked; + } else { + return R.string.tor_plugin_status_disabled; + } + } else { + return R.string.tor_plugin_status_inactive; + } + } + + @StringRes + private int getWifiPluginStatus(State state) { + if (state == ENABLING) return R.string.lan_plugin_status_enabling; + else if (state == ACTIVE) return R.string.lan_plugin_status_active; + else if (state == DISABLED) return R.string.lan_plugin_status_disabled; + else return R.string.lan_plugin_status_inactive; + } + + @StringRes + private int getBtPluginStatus(State state) { + if (state == ENABLING) return R.string.bt_plugin_status_enabling; + else if (state == ACTIVE) return R.string.bt_plugin_status_active; + else if (state == DISABLED) return R.string.bt_plugin_status_disabled; + else return R.string.bt_plugin_status_inactive; + } + + private Transport createTransport(TransportId id, + @DrawableRes int iconDrawable, @StringRes int title, + @StringRes int switchLabel, @StringRes int summary, + @StringRes int deviceStatus, @StringRes int pluginStatus) { + int iconColor = getIconColor(STARTING_STOPPING); + Transport transport = new Transport(id, iconDrawable, iconColor, title, + switchLabel, false, summary, deviceStatus, pluginStatus, false); + viewModel.getPluginState(id).observe(this, state -> { + transport.iconColor = getIconColor(state); + transport.pluginStatus = getPluginStatus(transport.id, state); + transportsAdapter.notifyDataSetChanged(); + }); + viewModel.getPluginEnabledSetting(id).observe(this, enabled -> { + transport.isSwitchChecked = enabled; + transportsAdapter.notifyDataSetChanged(); + }); + return transport; + } + + private static class Transport { + + private final TransportId id; + + @DrawableRes + private final int iconDrawable; + @StringRes + private final int title, switchLabel, summary; + + @ColorRes + private int iconColor; + @StringRes + private int deviceStatus, pluginStatus; + private boolean isSwitchChecked, showPluginStatus; + + private Transport(TransportId id, + @DrawableRes int iconDrawable, + @ColorRes int iconColor, + @StringRes int title, + @StringRes int switchLabel, + boolean isSwitchChecked, + @StringRes int summary, + @StringRes int deviceStatus, + @StringRes int pluginStatus, + boolean showPluginStatus) { + this.id = id; + this.iconDrawable = iconDrawable; + this.iconColor = iconColor; + this.title = title; + this.switchLabel = switchLabel; + this.isSwitchChecked = isSwitchChecked; + this.summary = summary; + this.deviceStatus = deviceStatus; + this.pluginStatus = pluginStatus; + this.showPluginStatus = showPluginStatus; + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/widget/TouchInterceptingLinearLayout.java b/briar-android/src/main/java/org/briarproject/briar/android/widget/TouchInterceptingLinearLayout.java new file mode 100644 index 000000000..8e3955c0b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/widget/TouchInterceptingLinearLayout.java @@ -0,0 +1,41 @@ +package org.briarproject.briar.android.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import androidx.annotation.Nullable; + +@NotNullByDefault +public class TouchInterceptingLinearLayout extends LinearLayout { + + public TouchInterceptingLinearLayout(Context context) { + super(context); + } + + public TouchInterceptingLinearLayout(Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TouchInterceptingLinearLayout(Context context, + @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public TouchInterceptingLinearLayout(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + onTouchEvent(e); + return false; + } +} diff --git a/briar-android/src/main/res/drawable/ic_settings_black_24dp.xml b/briar-android/src/main/res/drawable/ic_settings_black.xml similarity index 100% rename from briar-android/src/main/res/drawable/ic_settings_black_24dp.xml rename to briar-android/src/main/res/drawable/ic_settings_black.xml diff --git a/briar-android/src/main/res/layout/activity_transports.xml b/briar-android/src/main/res/layout/activity_transports.xml new file mode 100644 index 000000000..11f4f8dba --- /dev/null +++ b/briar-android/src/main/res/layout/activity_transports.xml @@ -0,0 +1,10 @@ + + diff --git a/briar-android/src/main/res/layout/list_item_transport_card.xml b/briar-android/src/main/res/layout/list_item_transport_card.xml new file mode 100644 index 000000000..638ef70a8 --- /dev/null +++ b/briar-android/src/main/res/layout/list_item_transport_card.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/layout/navigation_menu.xml b/briar-android/src/main/res/layout/navigation_menu.xml index 4a7a32c4f..38d257a2c 100644 --- a/briar-android/src/main/res/layout/navigation_menu.xml +++ b/briar-android/src/main/res/layout/navigation_menu.xml @@ -26,21 +26,13 @@ app:layout_constraintTop_toTopOf="parent" app:menu="@menu/navigation_drawer" /> - - - @@ -16,4 +17,4 @@ android:numColumns="3" tools:listitem="@layout/list_item_transport" /> - + diff --git a/briar-android/src/main/res/menu/navigation_drawer.xml b/briar-android/src/main/res/menu/navigation_drawer.xml index c24131755..ccfa71328 100644 --- a/briar-android/src/main/res/menu/navigation_drawer.xml +++ b/briar-android/src/main/res/menu/navigation_drawer.xml @@ -24,7 +24,7 @@ Lock App Settings Sign Out + Tap here to control how Briar connects to your contacts. - + Internet - Bluetooth + Your phone has Internet access via Wi-Fi + Your phone has Internet access via mobile data + Your phone does not have Internet access + Briar is connecting to the Internet + Briar is connected to the Internet + Briar can\'t connect to the Internet + Briar is configured not to use the Internet + Briar is configured not to use mobile data + Briar is configured not to use the Internet when running on battery + Briar is configured not to use the Internet in this country + + Wi-Fi + Same Wi-Fi network + Your phone is connected to Wi-Fi + Your phone is not connected to Wi-Fi + Briar is connecting to the Wi-Fi network + Briar is connected to the Wi-Fi network + Briar can\'t connect to the Wi-Fi network + Briar is configured not to use the Wi-Fi network + + + Bluetooth + Your phone\'s Bluetooth is turned on + Your phone\'s Bluetooth is turned off + Briar is connecting to Bluetooth + Briar is connected to Bluetooth + Briar can\'t connect to Bluetooth + Briar is configured not to use Bluetooth Signed out of Briar @@ -121,6 +149,7 @@ Help Sorry Unavailable on your system + Status: No contacts to show @@ -571,6 +600,9 @@ Briar is locked Tap to unlock + + Briar can connect to your contacts via the Internet, Wi-Fi or Bluetooth.\n\nAll Internet connections go through the Tor network for privacy.\n\nIf a contact can be reached by multiple methods, Briar uses them in parallel. +