diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index bf90f1b36..240ffd005 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -121,6 +121,7 @@ dependencies {
exclude group: 'com.android.support'
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
}
+ implementation 'org.nanohttpd:nanohttpd:2.3.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index e610211dd..4de40296e 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -18,6 +18,7 @@
+
diff --git a/briar-android/src/main/assets/hotspot.html b/briar-android/src/main/assets/hotspot.html
new file mode 100644
index 000000000..057e0ef42
--- /dev/null
+++ b/briar-android/src/main/assets/hotspot.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
Download Briar 1.2.20
+
+ Someone nearby shared Briar with you.
+
+
+
+ Download Briar
+
+
+ After the download is complete, open the downloaded file and install it.
+
+
+
+
Troubleshooting
+
+
If you can't download the app, try it with a different web
+ browser app.
+
+
Ensure that your browser is allowed to download apps directly by
+ giving it the permission or enabling the installation of apps from "Unknown Sources" in
+ system settings.
+
+ * Be sure to call {@link #onRequestPermissionResult(Boolean)} and
+ * {@link #onRequestWifiEnabledResult()} when you get the
+ * {@link ActivityResult}.
+ *
+ * As soon as {@link #checkAndRequestConditions()} returns true,
+ * all conditions are fulfilled.
+ */
+class ConditionManager {
+
+ private enum Permission {
+ UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
+ }
+
+ private Permission locationPermission = Permission.UNKNOWN;
+ private Permission wifiSetting = Permission.SHOW_RATIONALE;
+
+ private final FragmentActivity ctx;
+ private final WifiManager wifiManager;
+ private final ActivityResultLauncher locationRequest;
+ private final ActivityResultLauncher wifiRequest;
+
+ ConditionManager(FragmentActivity ctx,
+ ActivityResultLauncher locationRequest,
+ ActivityResultLauncher wifiRequest) {
+ this.ctx = ctx;
+ this.wifiManager = (WifiManager) ctx.getApplicationContext()
+ .getSystemService(WIFI_SERVICE);
+ this.locationRequest = locationRequest;
+ this.wifiRequest = wifiRequest;
+ }
+
+ /**
+ * Call this to reset state when UI starts,
+ * because state might have changed.
+ */
+ void resetPermissions() {
+ locationPermission = Permission.UNKNOWN;
+ wifiSetting = Permission.SHOW_RATIONALE;
+ }
+
+ /**
+ * This makes a request for location permission.
+ * If {@link #checkAndRequestConditions()} returns true, you can continue.
+ */
+ void startConditionChecks() {
+ locationRequest.launch(ACCESS_FINE_LOCATION);
+ }
+
+ /**
+ * @return true if conditions are fulfilled and flow can continue.
+ */
+ boolean checkAndRequestConditions() {
+ if (areEssentialPermissionsGranted()) return true;
+
+ // If an essential permission has been permanently denied, ask the
+ // user to change the setting
+ if (locationPermission == Permission.PERMANENTLY_DENIED) {
+ showDenialDialog(R.string.permission_location_title,
+ R.string.permission_hotspot_location_denied_body,
+ getGoToSettingsListener(ctx));
+ return false;
+ }
+ if (wifiSetting == Permission.PERMANENTLY_DENIED) {
+ showDenialDialog(R.string.wifi_settings_title,
+ R.string.wifi_settings_request_denied_body,
+ (d, w) -> requestEnableWiFi());
+ return false;
+ }
+
+ // Should we show the rationale for location permission or Wi-Fi?
+ if (locationPermission == Permission.SHOW_RATIONALE) {
+ showRationale(R.string.permission_location_title,
+ R.string.permission_hotspot_location_request_body,
+ this::requestPermissions);
+ } else if (wifiSetting == Permission.SHOW_RATIONALE) {
+ showRationale(R.string.wifi_settings_title,
+ R.string.wifi_settings_request_enable_body,
+ this::requestEnableWiFi);
+ }
+ return false;
+ }
+
+ void onRequestPermissionResult(Boolean granted) {
+ if (granted != null && granted) {
+ locationPermission = Permission.GRANTED;
+ } else if (shouldShowRequestPermissionRationale(ctx,
+ ACCESS_FINE_LOCATION)) {
+ locationPermission = Permission.SHOW_RATIONALE;
+ } else {
+ locationPermission = Permission.PERMANENTLY_DENIED;
+ }
+ }
+
+ void onRequestWifiEnabledResult() {
+ wifiSetting = wifiManager.isWifiEnabled() ? Permission.GRANTED :
+ Permission.PERMANENTLY_DENIED;
+ }
+
+ private boolean areEssentialPermissionsGranted() {
+ if (SDK_INT < 29) {
+ if (!wifiManager.isWifiEnabled()) {
+ //noinspection deprecation
+ return wifiManager.setWifiEnabled(true);
+ }
+ return true;
+ } else {
+ return locationPermission == Permission.GRANTED
+ && wifiManager.isWifiEnabled();
+ }
+ }
+
+ private void showDenialDialog(@StringRes int title, @StringRes int body,
+ OnClickListener onOkClicked) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setPositiveButton(R.string.ok, onOkClicked);
+ builder.setNegativeButton(R.string.cancel,
+ (dialog, which) -> ctx.supportFinishAfterTransition());
+ builder.show();
+ }
+
+ private void showRationale(@StringRes int title, @StringRes int body,
+ Runnable onContinueClicked) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle(title);
+ builder.setMessage(body);
+ builder.setNeutralButton(R.string.continue_button,
+ (dialog, which) -> onContinueClicked.run());
+ builder.show();
+ }
+
+ private void requestPermissions() {
+ locationRequest.launch(ACCESS_FINE_LOCATION);
+ }
+
+ private void requestEnableWiFi() {
+ Intent i = SDK_INT < 29 ?
+ new Intent(Settings.ACTION_WIFI_SETTINGS) :
+ new Intent(Settings.Panel.ACTION_WIFI);
+ wifiRequest.launch(i);
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
index 1d3076520..32a8e79ee 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotActivity.java
@@ -3,19 +3,25 @@ package org.briarproject.briar.android.hotspot;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
+import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
+import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
+import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.ActionBar;
+import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
+import static android.widget.Toast.LENGTH_LONG;
+import static org.briarproject.briar.android.util.UiUtils.showFragment;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_STOP_HOTSPOT;
@MethodsNotNullByDefault
@@ -44,7 +50,21 @@ public class HotspotActivity extends BriarActivity {
ab.setDisplayHomeAsUpEnabled(true);
}
- // TODO observe viewmodel state and show error or HotspotFragment
+ viewModel.getState().observe(this, hotspotState -> {
+ if (hotspotState instanceof HotspotStarted) {
+ FragmentManager fm = getSupportFragmentManager();
+ String tag = HotspotFragment.TAG;
+ // check if fragment is already added
+ // to not lose state on configuration changes
+ if (fm.findFragmentByTag(tag) == null) {
+ showFragment(fm, new HotspotFragment(), tag);
+ }
+ } else if (hotspotState instanceof HotspotError) {
+ // TODO ErrorFragment
+ String error = ((HotspotError) hotspotState).getError();
+ Toast.makeText(this, error, LENGTH_LONG).show();
+ }
+ });
if (state == null) {
getSupportFragmentManager().beginTransaction()
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotFragment.java
index a85c4fbb2..4c145a015 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotFragment.java
@@ -5,11 +5,12 @@ import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.briar.R;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import static org.briarproject.briar.android.util.UiUtils.showFragment;
+
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotFragment extends AbstractTabsFragment {
@@ -21,14 +22,9 @@ public class HotspotFragment extends AbstractTabsFragment {
super.onViewCreated(view, savedInstanceState);
// no need to call into the ViewModel here
connectedButton.setOnClickListener(v -> {
- getParentFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.step_next_in,
- R.anim.step_previous_out, R.anim.step_previous_in,
- R.anim.step_next_out)
- .replace(R.id.fragmentContainer, new WebsiteFragment(),
- WebsiteFragment.TAG)
- .addToBackStack(WebsiteFragment.TAG)
- .commit();
+ Fragment f = new WebsiteFragment();
+ String tag = WebsiteFragment.TAG;
+ showFragment(getParentFragmentManager(), f, tag);
});
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotIntroFragment.java
index ab32ac6c6..b1b34fe5a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotIntroFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotIntroFragment.java
@@ -1,6 +1,9 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,19 +12,27 @@ import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
+import com.google.android.material.snackbar.Snackbar;
+
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.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
+import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.transition.TransitionManager.beginDelayedTransition;
+import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@@ -34,13 +45,32 @@ public class HotspotIntroFragment extends Fragment {
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
+ private ConditionManager conditionManager;
+
+ private Button startButton;
+ private ProgressBar progressBar;
+ private TextView progressTextView;
+
+ private final ActivityResultLauncher locationRequest =
+ registerForActivityResult(new RequestPermission(), granted -> {
+ conditionManager.onRequestPermissionResult(granted);
+ startHotspot();
+ });
+ private final ActivityResultLauncher wifiRequest =
+ registerForActivityResult(new StartActivityForResult(), result -> {
+ conditionManager.onRequestWifiEnabledResult();
+ startHotspot();
+ });
@Override
public void onAttach(Context context) {
super.onAttach(context);
- getAndroidComponent(requireContext()).inject(this);
- viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
+ FragmentActivity activity = requireActivity();
+ getAndroidComponent(activity).inject(this);
+ viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(HotspotViewModel.class);
+ conditionManager =
+ new ConditionManager(activity, locationRequest, wifiRequest);
}
@Override
@@ -50,31 +80,48 @@ public class HotspotIntroFragment extends Fragment {
View v = inflater
.inflate(R.layout.fragment_hotspot_intro, container, false);
- Button startButton = v.findViewById(R.id.startButton);
- ProgressBar progressBar = v.findViewById(R.id.progressBar);
- TextView progressTextView = v.findViewById(R.id.progressTextView);
+ startButton = v.findViewById(R.id.startButton);
+ progressBar = v.findViewById(R.id.progressBar);
+ progressTextView = v.findViewById(R.id.progressTextView);
- startButton.setOnClickListener(button -> {
- beginDelayedTransition((ViewGroup) v);
- startButton.setVisibility(INVISIBLE);
- progressBar.setVisibility(VISIBLE);
- progressTextView.setVisibility(VISIBLE);
- // TODO remove below, tell viewModel to start hotspot instead
- v.postDelayed(() -> {
- viewModel.startHotspot();
- getParentFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.step_next_in,
- R.anim.step_previous_out,
- R.anim.step_previous_in,
- R.anim.step_next_out)
- .replace(R.id.fragmentContainer, new HotspotFragment(),
- HotspotFragment.TAG)
- .addToBackStack(HotspotFragment.TAG)
- .commit();
- }, 1500);
- });
+ startButton.setOnClickListener(
+ button -> conditionManager.startConditionChecks());
return v;
}
+ @Override
+ public void onStart() {
+ super.onStart();
+ conditionManager.resetPermissions();
+ }
+
+ private void startHotspot() {
+ if (conditionManager.checkAndRequestConditions()) {
+ showInstallWarningIfNeeded();
+ beginDelayedTransition((ViewGroup) requireView());
+ startButton.setVisibility(INVISIBLE);
+ progressBar.setVisibility(VISIBLE);
+ progressTextView.setVisibility(VISIBLE);
+ viewModel.startHotspot();
+ }
+ }
+
+ private void showInstallWarningIfNeeded() {
+ Context ctx = requireContext();
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = ctx.getPackageManager()
+ .getApplicationInfo(ctx.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AssertionError(e);
+ }
+ // test only apps can not be installed
+ if ((applicationInfo.flags & FLAG_TEST_ONLY) == FLAG_TEST_ONLY) {
+ int color = getResources().getColor(R.color.briar_red_500);
+ Snackbar.make(requireView(), R.string.hotspot_flag_test,
+ LENGTH_LONG).setBackgroundTint(color).show();
+ }
+ }
+
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotManager.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotManager.java
new file mode 100644
index 000000000..fb618c1c8
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotManager.java
@@ -0,0 +1,299 @@
+package org.briarproject.briar.android.hotspot;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pConfig;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.net.wifi.p2p.WifiP2pManager.ActionListener;
+import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
+import org.briarproject.briar.android.util.QrCodeUtils;
+
+import java.security.SecureRandom;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.UiThread;
+
+import static android.content.Context.WIFI_P2P_SERVICE;
+import static android.content.Context.WIFI_SERVICE;
+import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
+import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
+import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
+import static android.net.wifi.p2p.WifiP2pManager.BUSY;
+import static android.net.wifi.p2p.WifiP2pManager.ERROR;
+import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
+import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
+import static android.os.Build.VERSION.SDK_INT;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Logger.getLogger;
+
+class HotspotManager implements ActionListener {
+
+ interface HotspotListener {
+ void onStartingHotspot();
+
+ @IoExecutor
+ void onHotspotStarted(NetworkConfig networkConfig);
+
+ void onHotspotStopped();
+
+ void onHotspotError(String error);
+ }
+
+ private static final Logger LOG = getLogger(HotspotManager.class.getName());
+
+ private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
+ private static final int RETRY_DELAY_MILLIS = 1000;
+
+ private final Context ctx;
+ @IoExecutor
+ private final Executor ioExecutor;
+ private final SecureRandom random;
+ private final HotspotListener listener;
+ private final WifiManager wifiManager;
+ private final WifiP2pManager wifiP2pManager;
+ private final Handler handler;
+ private final String lockTag;
+
+ @Nullable
+ // on API < 29 this is null because we cannot request a custom network name
+ private String networkName = null;
+
+ private WifiManager.WifiLock wifiLock;
+ private WifiP2pManager.Channel channel;
+
+ HotspotManager(Context ctx, @IoExecutor Executor ioExecutor,
+ SecureRandom random, HotspotListener listener) {
+ this.ctx = ctx;
+ this.ioExecutor = ioExecutor;
+ this.random = random;
+ this.listener = listener;
+ wifiManager = (WifiManager) ctx.getApplicationContext()
+ .getSystemService(WIFI_SERVICE);
+ wifiP2pManager =
+ (WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
+ handler = new Handler(ctx.getMainLooper());
+ lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
+ }
+
+ @UiThread
+ void startWifiP2pHotspot() {
+ if (wifiP2pManager == null) {
+ listener.onHotspotError(
+ ctx.getString(R.string.hotspot_error_no_wifi_direct));
+ return;
+ }
+ listener.onStartingHotspot();
+ channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
+ if (channel == null) {
+ listener.onHotspotError(
+ ctx.getString(R.string.hotspot_error_no_wifi_direct));
+ return;
+ }
+ acquireLock();
+ try {
+ if (SDK_INT >= 29) {
+ networkName = getNetworkName();
+ String passphrase = getPassphrase();
+ WifiP2pConfig config = new WifiP2pConfig.Builder()
+ .setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
+ .setNetworkName(networkName)
+ .setPassphrase(passphrase)
+ .build();
+ wifiP2pManager.createGroup(channel, config, this);
+ } else {
+ wifiP2pManager.createGroup(channel, this);
+ }
+ } catch (SecurityException e) {
+ // this should never happen, because we request permissions before
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ // Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
+ public void onSuccess() {
+ requestGroupInfo(1);
+ }
+
+ @Override
+ // Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
+ public void onFailure(int reason) {
+ if (reason == BUSY) {
+ // Hotspot already running
+ requestGroupInfo(1);
+ } else if (reason == P2P_UNSUPPORTED) {
+ releaseHotspotWithError(ctx.getString(
+ R.string.hotspot_error_start_callback_failed,
+ "p2p unsupported"));
+ } else if (reason == ERROR) {
+ releaseHotspotWithError(ctx.getString(
+ R.string.hotspot_error_start_callback_failed, "p2p error"));
+ } else if (reason == NO_SERVICE_REQUESTS) {
+ releaseHotspotWithError(ctx.getString(
+ R.string.hotspot_error_start_callback_failed,
+ "no service requests"));
+ } else {
+ // all cases covered, in doubt set to error
+ releaseHotspotWithError(ctx.getString(
+ R.string.hotspot_error_start_callback_failed_unknown,
+ reason));
+ }
+ }
+
+ @RequiresApi(29)
+ private String getNetworkName() {
+ return "DIRECT-" + getRandomString(2) + "-" +
+ getRandomString(10);
+ }
+
+ private String getPassphrase() {
+ return getRandomString(8);
+ }
+
+ void stopWifiP2pHotspot() {
+ if (channel == null) return;
+ wifiP2pManager.removeGroup(channel, new ActionListener() {
+ @Override
+ public void onSuccess() {
+ releaseHotspot();
+ }
+
+ @Override
+ public void onFailure(int reason) {
+ // not propagating back error
+ releaseHotspot();
+ }
+ });
+ }
+
+ private void acquireLock() {
+ // WIFI_MODE_FULL has no effect on API >= 29
+ int lockType =
+ SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
+ wifiLock = wifiManager.createWifiLock(lockType, lockTag);
+ wifiLock.acquire();
+ }
+
+ private void releaseHotspot() {
+ listener.onHotspotStopped();
+ closeChannelAndReleaseLock();
+ }
+
+ private void releaseHotspotWithError(String error) {
+ listener.onHotspotError(error);
+ closeChannelAndReleaseLock();
+ }
+
+ private void closeChannelAndReleaseLock() {
+ if (SDK_INT >= 27) channel.close();
+ channel = null;
+ wifiLock.release();
+ }
+
+ private void requestGroupInfo(int attempt) {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("requestGroupInfo attempt: " + attempt);
+ }
+ GroupInfoListener groupListener = group -> {
+ boolean valid = isGroupValid(group);
+ // If the group is valid, set the hotspot to started. If we don't
+ // have any attempts left, we try what we got
+ if (valid || attempt >= MAX_GROUP_INFO_ATTEMPTS) {
+ onHotspotStarted(group);
+ } else {
+ retryRequestingGroupInfo(attempt + 1);
+ }
+ };
+ try {
+ wifiP2pManager.requestGroupInfo(channel, groupListener);
+ } catch (SecurityException e) {
+ // this should never happen, because we request permissions before
+ throw new AssertionError(e);
+ }
+ }
+
+ private void onHotspotStarted(WifiP2pGroup group) {
+ DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
+ ioExecutor.execute(() -> {
+ String content = createWifiLoginString(group.getNetworkName(),
+ group.getPassphrase());
+ Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
+ NetworkConfig config = new NetworkConfig(group.getNetworkName(),
+ group.getPassphrase(), qrCode);
+ listener.onHotspotStarted(config);
+ });
+ }
+
+ private boolean isGroupValid(@Nullable WifiP2pGroup group) {
+ if (group == null) {
+ LOG.info("group is null");
+ return false;
+ } else if (!group.getNetworkName().startsWith("DIRECT-")) {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("received networkName without prefix 'DIRECT-': " +
+ group.getNetworkName());
+ }
+ return false;
+ } else if (networkName != null &&
+ !networkName.equals(group.getNetworkName())) {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("expected networkName: " + networkName);
+ LOG.info("received networkName: " + group.getNetworkName());
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private void retryRequestingGroupInfo(int attempt) {
+ LOG.info("retrying");
+ // On some devices we need to wait for the group info to become available
+ if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
+ handler.postDelayed(() -> requestGroupInfo(attempt + 1),
+ RETRY_DELAY_MILLIS);
+ } else {
+ releaseHotspotWithError(ctx.getString(
+ R.string.hotspot_error_start_callback_no_group_info));
+ }
+ }
+
+ private static String createWifiLoginString(String ssid, String password) {
+ // https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
+ // do not remove the dangling ';', it can cause problems to omit it
+ return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
+ }
+
+ private static final String digits = "123456789"; // avoid 0
+ private static final String letters = "abcdefghijkmnopqrstuvwxyz"; // no l
+ private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no I, O
+
+ private String getRandomString(int length) {
+ char[] c = new char[length];
+ for (int i = 0; i < length; i++) {
+ if (random.nextBoolean()) {
+ c[i] = random(digits);
+ } else if (random.nextBoolean()) {
+ c[i] = random(letters);
+ } else {
+ c[i] = random(LETTERS);
+ }
+ }
+ return new String(c);
+ }
+
+ private char random(String universe) {
+ return universe.charAt(random.nextInt(universe.length()));
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotState.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotState.java
new file mode 100644
index 000000000..40e620fcc
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotState.java
@@ -0,0 +1,66 @@
+package org.briarproject.briar.android.hotspot;
+
+import android.graphics.Bitmap;
+
+import androidx.annotation.Nullable;
+
+abstract class HotspotState {
+
+ static class StartingHotspot extends HotspotState {
+ }
+
+ static class NetworkConfig {
+ final String ssid, password;
+ @Nullable
+ final Bitmap qrCode;
+
+ NetworkConfig(String ssid, String password, @Nullable Bitmap qrCode) {
+ this.ssid = ssid;
+ this.password = password;
+ this.qrCode = qrCode;
+ }
+ }
+
+ static class WebsiteConfig {
+ final String url;
+ @Nullable
+ final Bitmap qrCode;
+
+ WebsiteConfig(String url, @Nullable Bitmap qrCode) {
+ this.url = url;
+ this.qrCode = qrCode;
+ }
+ }
+
+ static class HotspotStarted extends HotspotState {
+ private final NetworkConfig networkConfig;
+ private final WebsiteConfig websiteConfig;
+
+ HotspotStarted(NetworkConfig networkConfig,
+ WebsiteConfig websiteConfig) {
+ this.networkConfig = networkConfig;
+ this.websiteConfig = websiteConfig;
+ }
+
+ NetworkConfig getNetworkConfig() {
+ return networkConfig;
+ }
+
+ WebsiteConfig getWebsiteConfig() {
+ return websiteConfig;
+ }
+ }
+
+ static class HotspotError extends HotspotState {
+ private final String error;
+
+ HotspotError(String error) {
+ this.error = error;
+ }
+
+ String getError() {
+ return error;
+ }
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java
index 4dfc3c733..e4b13a598 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/HotspotViewModel.java
@@ -4,37 +4,82 @@ import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.TransactionManager;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.hotspot.HotspotManager.HotspotListener;
+import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
+import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
+import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
+import org.briarproject.briar.android.hotspot.HotspotState.StartingHotspot;
+import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
+import org.briarproject.briar.android.hotspot.WebServerManager.WebServerListener;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.api.android.AndroidNotificationManager;
+import java.security.SecureRandom;
import java.util.concurrent.Executor;
+import java.util.logging.Logger;
import javax.inject.Inject;
-@NotNullByDefault
-class HotspotViewModel extends DbViewModel {
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import static java.util.logging.Logger.getLogger;
+
+@NotNullByDefault
+class HotspotViewModel extends DbViewModel
+ implements HotspotListener, WebServerListener {
+
+ private static final Logger LOG =
+ getLogger(HotspotViewModel.class.getName());
+
+ @IoExecutor
+ private final Executor ioExecutor;
private final AndroidNotificationManager notificationManager;
+ private final HotspotManager hotspotManager;
+ private final WebServerManager webServerManager;
+
+ private final MutableLiveData state =
+ new MutableLiveData<>();
+
+ @Nullable
+ // Field to temporarily store the network config received via onHotspotStarted()
+ // in order to post it along with a HotspotStarted status
+ private volatile NetworkConfig networkConfig;
@Inject
- HotspotViewModel(Application application,
+ HotspotViewModel(Application app,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
+ @IoExecutor Executor ioExecutor,
+ SecureRandom secureRandom,
AndroidNotificationManager notificationManager) {
- super(application, dbExecutor, lifecycleManager, db, androidExecutor);
+ super(app, dbExecutor, lifecycleManager, db, androidExecutor);
+ this.ioExecutor = ioExecutor;
this.notificationManager = notificationManager;
+ hotspotManager =
+ new HotspotManager(app, ioExecutor, secureRandom, this);
+ webServerManager = new WebServerManager(app, this);
}
+ @UiThread
void startHotspot() {
+ hotspotManager.startWifiP2pHotspot();
notificationManager.showHotspotNotification();
}
+ @UiThread
private void stopHotspot() {
+ ioExecutor.execute(webServerManager::stopWebServer);
+ hotspotManager.stopWifiP2pHotspot();
notificationManager.clearHotspotNotification();
}
@@ -44,6 +89,49 @@ class HotspotViewModel extends DbViewModel {
stopHotspot();
}
- // TODO copy actual code from Offline Hotspot app
+ @Override
+ public void onStartingHotspot() {
+ state.setValue(new StartingHotspot());
+ }
+
+ @Override
+ @IoExecutor
+ public void onHotspotStarted(NetworkConfig networkConfig) {
+ this.networkConfig = networkConfig;
+ LOG.info("starting webserver");
+ webServerManager.startWebServer();
+ }
+
+ @Override
+ public void onHotspotStopped() {
+ LOG.info("stopping webserver");
+ ioExecutor.execute(webServerManager::stopWebServer);
+ }
+
+ @Override
+ public void onHotspotError(String error) {
+ state.setValue(new HotspotError(error));
+ ioExecutor.execute(webServerManager::stopWebServer);
+ notificationManager.clearHotspotNotification();
+ }
+
+ @Override
+ @IoExecutor
+ public void onWebServerStarted(WebsiteConfig websiteConfig) {
+ state.postValue(new HotspotStarted(networkConfig, websiteConfig));
+ networkConfig = null;
+ }
+
+ @Override
+ @IoExecutor
+ public void onWebServerError() {
+ state.postValue(new HotspotError(getApplication()
+ .getString(R.string.hotspot_error_web_server_start)));
+ hotspotManager.stopWifiP2pHotspot();
+ }
+
+ LiveData getState() {
+ return state;
+ }
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ManualHotspotFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ManualHotspotFragment.java
index d8ea980fd..f049eab18 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ManualHotspotFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ManualHotspotFragment.java
@@ -14,12 +14,14 @@ import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
+import static org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -66,21 +68,28 @@ public class ManualHotspotFragment extends Fragment {
TextView passwordView = v.findViewById(R.id.passwordView);
TextView altView = v.findViewById(R.id.altView);
+ Consumer consumer;
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
manualIntroView.setText(R.string.hotspot_manual_wifi);
ssidLabelView.setText(R.string.hotspot_manual_wifi_ssid);
- // TODO observe state in ViewModel and get info from there instead
- ssidView.setText("DIRECT-42-dfzsgf34ef");
- passwordView.setText("sdf78shfd8");
+ consumer = state -> {
+ ssidView.setText(state.getNetworkConfig().ssid);
+ passwordView.setText(state.getNetworkConfig().password);
+ };
altView.setText(R.string.hotspot_manual_wifi_alt);
} else {
manualIntroView.setText(R.string.hotspot_manual_site);
ssidLabelView.setText(R.string.hotspot_manual_site_address);
- // TODO observe state in ViewModel and get info from there instead
- ssidView.setText("http://192.168.49.1:9999");
+ consumer = state -> ssidView.setText(state.getWebsiteConfig().url);
altView.setText(R.string.hotspot_manual_site_alt);
v.findViewById(R.id.passwordLabelView).setVisibility(GONE);
passwordView.setVisibility(GONE);
}
+ viewModel.getState().observe(getViewLifecycleOwner(), state -> {
+ // we only expect to be in this state here
+ if (state instanceof HotspotStarted) {
+ consumer.accept((HotspotStarted) state);
+ }
+ });
}
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/QrHotspotFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/QrHotspotFragment.java
index 84a56c8bd..f56f51580 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/QrHotspotFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/QrHotspotFragment.java
@@ -11,10 +11,12 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
+import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import javax.inject.Inject;
import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -58,13 +60,21 @@ public class QrHotspotFragment extends Fragment {
TextView qrIntroView = v.findViewById(R.id.qrIntroView);
ImageView qrCodeView = v.findViewById(R.id.qrCodeView);
+ Consumer consumer;
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
qrIntroView.setText(R.string.hotspot_qr_wifi);
- // TODO observe state in ViewModel and get QR code from there
+ consumer = state ->
+ qrCodeView.setImageBitmap(state.getNetworkConfig().qrCode);
} else {
qrIntroView.setText(R.string.hotspot_qr_site);
- // TODO observe state in ViewModel and get QR code from there
+ consumer = state ->
+ qrCodeView.setImageBitmap(state.getWebsiteConfig().qrCode);
}
+ viewModel.getState().observe(getViewLifecycleOwner(), state -> {
+ if (state instanceof HotspotStarted) {
+ consumer.accept((HotspotStarted) state);
+ }
+ });
return v;
}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java
new file mode 100644
index 000000000..14dfba349
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServer.java
@@ -0,0 +1,130 @@
+package org.briarproject.briar.android.hotspot;
+
+import android.content.Context;
+
+import org.briarproject.briar.R;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import androidx.annotation.Nullable;
+import fi.iki.elonen.NanoHTTPD;
+
+import static android.util.Xml.Encoding.UTF_8;
+import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
+import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
+import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
+import static java.util.Objects.requireNonNull;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.BuildConfig.VERSION_NAME;
+
+public class WebServer extends NanoHTTPD {
+
+ final static int PORT = 9999;
+
+ private static final Logger LOG = getLogger(WebServer.class.getName());
+ private static final String FILE_HTML = "hotspot.html";
+ private static final Pattern REGEX_AGENT =
+ Pattern.compile("Android ([0-9]+)");
+
+ private final Context ctx;
+
+ WebServer(Context ctx) {
+ super(PORT);
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void start() throws IOException {
+ start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+ if (session.getUri().endsWith("favicon.ico")) {
+ return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
+ NOT_FOUND.getDescription());
+ }
+ if (session.getUri().endsWith(".apk")) {
+ return serveApk();
+ }
+ Response res;
+ try {
+ String html = getHtml(session.getHeaders().get("user-agent"));
+ res = newFixedLengthResponse(OK, MIME_HTML, html);
+ } catch (Exception e) {
+ logException(LOG, WARNING, e);
+ res = newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT,
+ INTERNAL_ERROR.getDescription());
+ }
+ return res;
+ }
+
+ private String getHtml(@Nullable String userAgent) throws Exception {
+ Document doc;
+ try (InputStream is = ctx.getAssets().open(FILE_HTML)) {
+ doc = Jsoup.parse(is, UTF_8.name(), "");
+ }
+ String app = ctx.getString(R.string.app_name);
+ String appV = app + " " + VERSION_NAME;
+ doc.select("#download_title").first()
+ .text(ctx.getString(R.string.website_download_title, appV));
+ doc.select("#download_intro").first()
+ .text(ctx.getString(R.string.website_download_intro, app));
+ doc.select("#download_button").first()
+ .text(ctx.getString(R.string.website_download_title, app));
+ doc.select("#download_outro").first()
+ .text(ctx.getString(R.string.website_download_outro));
+ doc.select("#troubleshooting_title").first()
+ .text(ctx.getString(R.string.website_troubleshooting_title));
+ doc.select("#troubleshooting_1").first()
+ .text(ctx.getString(R.string.website_troubleshooting_1));
+ doc.select("#troubleshooting_2").first()
+ .text(getUnknownSourcesString(userAgent));
+ return doc.outerHtml();
+ }
+
+ private String getUnknownSourcesString(String userAgent) {
+ boolean is8OrHigher = false;
+ if (userAgent != null) {
+ Matcher matcher = REGEX_AGENT.matcher(userAgent);
+ if (matcher.find()) {
+ int androidMajorVersion =
+ Integer.parseInt(requireNonNull(matcher.group(1)));
+ is8OrHigher = androidMajorVersion >= 8;
+ }
+ }
+ return is8OrHigher ?
+ ctx.getString(R.string.website_troubleshooting_2_new) :
+ ctx.getString(R.string.website_troubleshooting_2_old);
+ }
+
+ private Response serveApk() {
+ String mime = "application/vnd.android.package-archive";
+
+ File file = new File(ctx.getPackageCodePath());
+ long fileLen = file.length();
+
+ Response res;
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ res = newFixedLengthResponse(OK, mime, fis, fileLen);
+ res.addHeader("Content-Length", "" + fileLen);
+ } catch (FileNotFoundException e) {
+ logException(LOG, WARNING, e);
+ res = newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
+ "Error 404, file not found.");
+ }
+ return res;
+ }
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServerManager.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServerManager.java
new file mode 100644
index 000000000..cb01e8c1d
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/WebServerManager.java
@@ -0,0 +1,114 @@
+package org.briarproject.briar.android.hotspot;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.DisplayMetrics;
+
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
+import org.briarproject.briar.android.util.QrCodeUtils;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.logging.Logger;
+
+import androidx.annotation.Nullable;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.list;
+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.util.LogUtils.logException;
+import static org.briarproject.briar.android.hotspot.WebServer.PORT;
+
+class WebServerManager {
+
+ interface WebServerListener {
+ @IoExecutor
+ void onWebServerStarted(WebsiteConfig websiteConfig);
+
+ @IoExecutor
+ void onWebServerError();
+ }
+
+ private static final Logger LOG =
+ getLogger(WebServerManager.class.getName());
+
+ private final WebServer webServer;
+ private final WebServerListener listener;
+ private final DisplayMetrics dm;
+
+ WebServerManager(Context ctx, WebServerListener listener) {
+ this.listener = listener;
+ webServer = new WebServer(ctx);
+ dm = ctx.getResources().getDisplayMetrics();
+ }
+
+ @IoExecutor
+ void startWebServer() {
+ try {
+ webServer.start();
+ onWebServerStarted();
+ } catch (IOException e) {
+ logException(LOG, WARNING, e);
+ listener.onWebServerError();
+ }
+ }
+
+ @IoExecutor
+ private void onWebServerStarted() {
+ String url = "http://192.168.49.1:" + PORT;
+ InetAddress address = getAccessPointAddress();
+ if (address == null) {
+ LOG.info(
+ "Could not find access point address, assuming 192.168.49.1");
+ } else {
+ if (LOG.isLoggable(INFO)) {
+ LOG.info("Access point address " + address.getHostAddress());
+ }
+ url = "http://" + address.getHostAddress() + ":" + PORT;
+ }
+ Bitmap qrCode = QrCodeUtils.createQrCode(dm, url);
+ listener.onWebServerStarted(new WebsiteConfig(url, qrCode));
+ }
+
+ /**
+ * It is safe to call this more than once and it won't throw.
+ */
+ @IoExecutor
+ void stopWebServer() {
+ webServer.stop();
+ }
+
+ @Nullable
+ private static InetAddress getAccessPointAddress() {
+ for (NetworkInterface i : getNetworkInterfaces()) {
+ if (i.getName().startsWith("p2p")) {
+ for (InterfaceAddress a : i.getInterfaceAddresses()) {
+ // we consider only IPv4 addresses
+ if (a.getAddress().getAddress().length == 4)
+ return a.getAddress();
+ }
+ }
+ }
+ return null;
+ }
+
+ private static List getNetworkInterfaces() {
+ try {
+ Enumeration ifaces =
+ NetworkInterface.getNetworkInterfaces();
+ return ifaces == null ? emptyList() : list(ifaces);
+ } catch (SocketException e) {
+ logException(LOG, WARNING, e);
+ return emptyList();
+ }
+ }
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/QrCodeUtils.java
similarity index 91%
rename from briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java
rename to briar-android/src/main/java/org/briarproject/briar/android/util/QrCodeUtils.java
index 26f2bf050..ae8fa55e6 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/QrCodeUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/QrCodeUtils.java
@@ -1,4 +1,4 @@
-package org.briarproject.briar.android.contact.add.nearby;
+package org.briarproject.briar.android.util;
import android.graphics.Bitmap;
import android.util.DisplayMetrics;
@@ -22,12 +22,12 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
-class QrCodeUtils {
+public class QrCodeUtils {
private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
@Nullable
- static Bitmap createQrCode(DisplayMetrics dm, String input) {
+ public static Bitmap createQrCode(DisplayMetrics dm, String input) {
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
try {
// Generate QR code
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
index aa77231d7..ab03767f3 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
@@ -58,7 +58,9 @@ import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.text.HtmlCompat;
import androidx.core.util.Consumer;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -139,6 +141,17 @@ public class UiUtils {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
+ public static void showFragment(FragmentManager fm, Fragment f,
+ @Nullable String tag) {
+ fm.beginTransaction()
+ .setCustomAnimations(R.anim.step_next_in,
+ R.anim.step_previous_out, R.anim.step_previous_in,
+ R.anim.step_next_out)
+ .replace(R.id.fragmentContainer, f, tag)
+ .addToBackStack(tag)
+ .commit();
+ }
+
public static String getContactDisplayName(Author author,
@Nullable String alias) {
String name = author.getName();
diff --git a/briar-android/src/main/res/layout/fragment_hotspot_qr.xml b/briar-android/src/main/res/layout/fragment_hotspot_qr.xml
index 2cfa4932e..4a55b0641 100644
--- a/briar-android/src/main/res/layout/fragment_hotspot_qr.xml
+++ b/briar-android/src/main/res/layout/fragment_hotspot_qr.xml
@@ -17,35 +17,30 @@
android:layout_margin="16dp"
android:text="@string/hotspot_qr_wifi"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardView"
app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintVertical_bias="0.0">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:ignore="ContentDescription"
+ tools:src="@tools:sample/avatars" />
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index bfcf491cd..24b99bfd5 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -701,6 +701,20 @@
Wi-Fi hotspotSharing Briar offlineConfirm connection
+
+ To create a Wi-Fi hotspot, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.
+ You have denied access to your location, but Briar needs this permission to create a Wi-Fi hotspot.\n\nPlease consider granting access.
+ Wi-Fi setting
+ To create a Wi-Fi hotspot, Briar needs to use Wi-Fi. Please enable it.
+ You have denied to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.
+
+ Device does not support Wi-Fi Direct
+ Hotspot failed to start: error %s
+ Hotspot failed to start with an unknown error, reason %d
+ Hotspot failed to start: no group info
+ Error starting web server!
+ Warning: This is a debug app that can NOT be installed on another device
+
ManualTo download the app on another phone, please connect to this Wi-Fi network:Network name (SSID)
@@ -710,6 +724,16 @@
Instead of typing the address manually, you can also scan a QR code.To download the app on another phone, please scan this QR code to connect to this Wi-Fi network:After you are connected to the Wi-Fi, scan this QR code to download the app.
+
+
+ Download %s
+ Someone nearby shared %s with you.
+ After the download is complete, open the downloaded file and install it.
+ Troubleshooting
+ If you cannot download the app, try it with a different web browser app.
+ To install the downloaded app, you might need to allow installation of apps from \"Unknown sources\" in system settings. Afterwards, you may need to download the app again.
+ To install the downloaded app, you might need to allow your browser to install unknown apps.
+
Problems with connecting to Wi-Fi:Try disabling and re-enabling Wi-Fi on both phones and try again.If your phone complains that the Wi-Fi has no internet, tell it that you want to stay connected anyway.
diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle
index 6b11c5cdc..4ae036ef9 100644
--- a/briar-android/witness.gradle
+++ b/briar-android/witness.gradle
@@ -216,6 +216,7 @@ dependencyVerification {
'org.jmock:jmock:2.8.2:jmock-2.8.2.jar:6c73cb4a2e6dbfb61fd99c9a768539c170ab6568e57846bd60dbf19596b65b16',
'org.jvnet.staxex:stax-ex:1.8:stax-ex-1.8.jar:95b05d9590af4154c6513b9c5dc1fb2e55b539972ba0a9ef28e9a0c01d83ad77',
'org.mockito:mockito-core:3.1.0:mockito-core-3.1.0.jar:89b09e518e04f5c35f5ccf7abe45e72f594070a53d95cc2579001bd392c5afa6',
+ 'org.nanohttpd:nanohttpd:2.3.1:nanohttpd-2.3.1.jar:de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58',
'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d',
'org.ow2.asm:asm-analysis:7.0:asm-analysis-7.0.jar:e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474',
'org.ow2.asm:asm-commons:7.0:asm-commons-7.0.jar:fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d',