From 355c487ec9f791ace6420490eddea7390d5d98fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCrten?= Date: Wed, 21 Jul 2021 11:34:31 +0200 Subject: [PATCH] Split ConditionManager into API-specific versions * On API 29+ we need the location permission to start the hotspot, while on lower API levels, we don't. In order to handle permissions and other conditions in a clear manner depending the API level of the device the app is running on, have separate extensions of the base ConditionManager class. * Take special care to handle situations gracefully where the Wifi is disabled and the user tries to start the hotspot. We cannot simply rely on Wifi being enabled as a sufficient condition that allows us to start the hotspot. We need to wait for WifiP2p to be available. While it is tricky to obtain that state (it involves registering a broadcast receiver for the WIFI_P2P_STATE_CHANGED_ACTION broadcast, keeping track of changes there and even then things are still ugly. It can happen that WifiP2p is available *before* Wifi is. Also it can happen that WifiP2p never becomes available because some other application has already opened a hotspot. Instead of checking that state, we now just try (and retry repeatedly after a delay) to start the hotspot (and the WifiP2p framework) hoping that is becomes availabe within a reasonable amount of time after Wifi has been detected to be on. Currently we try 5 times with a delay of 1 second. * Improve the behavior of disabling and re-enabling the 'start hotspot' button, so that it becomes impossible to double-tap it, but still making sure that the button get re-enabled as soon as the UI is back in a state where the user should be able to tap the button again. --- .../android/hotspot/ConditionManager.java | 166 +++--------------- .../hotspot/ConditionManager29Impl.java | 136 ++++++++++++++ .../android/hotspot/ConditionManagerImpl.java | 83 +++++++++ .../android/hotspot/HotspotIntroFragment.java | 43 +++-- .../briar/android/hotspot/HotspotManager.java | 112 ++++++++---- .../android/hotspot/HotspotViewModel.java | 2 +- .../briar/android/util/UiUtils.java | 29 +++ briar-android/src/main/res/values/strings.xml | 1 + 8 files changed, 375 insertions(+), 197 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29Impl.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManagerImpl.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager.java index c0fc3b3f0..ba9119535 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager.java @@ -1,171 +1,53 @@ package org.briarproject.briar.android.hotspot; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; import android.net.wifi.WifiManager; -import android.provider.Settings; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.R; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; +import androidx.core.util.Consumer; import androidx.fragment.app.FragmentActivity; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.content.Context.WIFI_SERVICE; -import static android.os.Build.VERSION.SDK_INT; -import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; -import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; /** - * This class ensures that the conditions to open a hotspot are fulfilled. - *

- * 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. + * Abstract base class for the ConditionManagers that ensure that the conditions + * to open a hotspot are fulfilled. There are different extensions of this for + * API levels lower than 29 and 29+. */ -@NotNullByDefault -class ConditionManager { +abstract class ConditionManager { - private enum Permission { + enum Permission { UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED } - private Permission locationPermission = Permission.UNKNOWN; - private Permission wifiSetting = Permission.SHOW_RATIONALE; + protected final Consumer permissionUpdateCallback; + protected FragmentActivity ctx; + protected WifiManager wifiManager; - private final FragmentActivity ctx; - private final WifiManager wifiManager; - private final ActivityResultLauncher locationRequest; - private final ActivityResultLauncher wifiRequest; + ConditionManager(Consumer permissionUpdateCallback) { + this.permissionUpdateCallback = permissionUpdateCallback; + } - ConditionManager(FragmentActivity ctx, - ActivityResultLauncher locationRequest, - ActivityResultLauncher wifiRequest) { + /** + * Pass a FragmentActivity context here during `onCreateView()`. + */ + void init(FragmentActivity ctx) { 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. + * Call this during onStart() in the fragment where the ConditionManager + * is used. */ - 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); - } + abstract void onStart(); /** + * Check if all required conditions are met such that the hotspot can be + * started. If any precondition is not met yet, bring up relevant dialogs + * asking the user to grant relevant permissions or take relevant actions. + * * @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(@Nullable 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); - } + abstract boolean checkAndRequestConditions(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29Impl.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29Impl.java new file mode 100644 index 000000000..94d79f382 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManager29Impl.java @@ -0,0 +1,136 @@ +package org.briarproject.briar.android.hotspot; + +import android.content.Intent; +import android.provider.Settings; + +import org.briarproject.briar.R; + +import java.util.logging.Logger; + +import androidx.activity.result.ActivityResultCaller; +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.annotation.RequiresApi; +import androidx.core.util.Consumer; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener; +import static org.briarproject.briar.android.util.UiUtils.showDenialDialog; +import static org.briarproject.briar.android.util.UiUtils.showRationale; + +/** + * This class ensures that the conditions to open a hotspot are fulfilled on + * API levels >= 29. + *

+ * As soon as {@link #checkAndRequestConditions()} returns true, + * all conditions are fulfilled. + */ +@RequiresApi(29) +class ConditionManager29Impl extends ConditionManager { + + private static final Logger LOG = + getLogger(ConditionManager29Impl.class.getName()); + + private Permission locationPermission = Permission.UNKNOWN; + + private final ActivityResultLauncher locationRequest; + private final ActivityResultLauncher wifiRequest; + + ConditionManager29Impl(ActivityResultCaller arc, + Consumer permissionUpdateCallback) { + super(permissionUpdateCallback); + locationRequest = arc.registerForActivityResult( + new RequestPermission(), granted -> { + onRequestPermissionResult(granted); + permissionUpdateCallback.accept(true); + }); + wifiRequest = arc.registerForActivityResult( + new StartActivityForResult(), + result -> permissionUpdateCallback.accept(true)); + } + + @Override + void onStart() { + locationPermission = Permission.UNKNOWN; + } + + private boolean areEssentialPermissionsGranted() { + if (LOG.isLoggable(INFO)) { + LOG.info(String.format("areEssentialPermissionsGranted(): " + + "locationPermission? %s, " + + "wifiManager.isWifiEnabled()? %b", + locationPermission, + wifiManager.isWifiEnabled())); + } + return locationPermission == Permission.GRANTED && + wifiManager.isWifiEnabled(); + } + + @Override + boolean checkAndRequestConditions() { + if (areEssentialPermissionsGranted()) return true; + + if (locationPermission == Permission.UNKNOWN) { + locationRequest.launch(ACCESS_FINE_LOCATION); + return false; + } + + // If the location permission has been permanently denied, ask the + // user to change the setting + if (locationPermission == Permission.PERMANENTLY_DENIED) { + showDenialDialog(ctx, R.string.permission_location_title, + R.string.permission_hotspot_location_denied_body, + getGoToSettingsListener(ctx), + () -> permissionUpdateCallback.accept(false)); + return false; + } + + // Should we show the rationale for location permission? + if (locationPermission == Permission.SHOW_RATIONALE) { + showRationale(ctx, R.string.permission_location_title, + R.string.permission_hotspot_location_request_body, + this::requestPermissions, + () -> permissionUpdateCallback.accept(false)); + return false; + } + + // If Wifi is not enabled, we show the rationale for enabling Wifi? + if (!wifiManager.isWifiEnabled()) { + showRationale(ctx, R.string.wifi_settings_title, + R.string.wifi_settings_request_enable_body, + this::requestEnableWiFi, + () -> permissionUpdateCallback.accept(false)); + return false; + } + + // we shouldn't usually reach this point, but if we do, return false + // anyway to force a recheck. Maybe some condition changed in the + // meantime. + return false; + } + + private void onRequestPermissionResult(@Nullable 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; + } + } + + private void requestPermissions() { + locationRequest.launch(ACCESS_FINE_LOCATION); + } + + private void requestEnableWiFi() { + wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI)); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManagerImpl.java new file mode 100644 index 000000000..456f89ae3 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/ConditionManagerImpl.java @@ -0,0 +1,83 @@ +package org.briarproject.briar.android.hotspot; + +import android.content.Intent; +import android.provider.Settings; + +import org.briarproject.briar.R; + +import java.util.logging.Logger; + +import androidx.activity.result.ActivityResultCaller; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; +import androidx.core.util.Consumer; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.briar.android.util.UiUtils.showRationale; + +/** + * This class ensures that the conditions to open a hotspot are fulfilled on + * API levels < 29. + *

+ * As soon as {@link #checkAndRequestConditions()} returns true, + * all conditions are fulfilled. + */ +class ConditionManagerImpl extends ConditionManager { + + private static final Logger LOG = + getLogger(ConditionManagerImpl.class.getName()); + + private final ActivityResultLauncher wifiRequest; + + ConditionManagerImpl(ActivityResultCaller arc, + Consumer permissionUpdateCallback) { + super(permissionUpdateCallback); + wifiRequest = arc.registerForActivityResult( + new StartActivityForResult(), + result -> permissionUpdateCallback.accept(true)); + } + + @Override + void onStart() { + // nothing to do here + } + + private boolean areEssentialPermissionsGranted() { + if (LOG.isLoggable(INFO)) { + LOG.info(String.format("areEssentialPermissionsGranted(): " + + "wifiManager.isWifiEnabled()? %b", + wifiManager.isWifiEnabled())); + } + return wifiManager.isWifiEnabled(); + } + + @Override + boolean checkAndRequestConditions() { + if (areEssentialPermissionsGranted()) return true; + + if (!wifiManager.isWifiEnabled()) { + // Try enabling the Wifi and return true if that seems to have been + // successful, i.e. "Wifi is either already in the requested state, or + // in progress toward the requested state". + if (wifiManager.setWifiEnabled(true)) { + LOG.info("Enabled wifi"); + return true; + } + + // Wifi is not enabled and we can't seem to enable it, so ask the user + // to enable it for us. + showRationale(ctx, R.string.wifi_settings_title, + R.string.wifi_settings_request_enable_body, + this::requestEnableWiFi, + () -> permissionUpdateCallback.accept(false)); + } + + return false; + } + + private void requestEnableWiFi() { + wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS)); + } + +} 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 7979cb956..e5f9e6b50 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,7 +1,6 @@ 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; @@ -20,15 +19,13 @@ 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.os.Build.VERSION.SDK_INT; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static androidx.transition.TransitionManager.beginDelayedTransition; @@ -45,22 +42,14 @@ 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(); - }); + private final ConditionManager conditionManager = SDK_INT < 29 ? + new ConditionManagerImpl(this, this::onPermissionUpdate) : + new ConditionManager29Impl(this, this::onPermissionUpdate); @Override public void onAttach(Context context) { @@ -69,8 +58,6 @@ public class HotspotIntroFragment extends Fragment { getAndroidComponent(activity).inject(this); viewModel = new ViewModelProvider(activity, viewModelFactory) .get(HotspotViewModel.class); - conditionManager = - new ConditionManager(activity, locationRequest, wifiRequest); } @Override @@ -84,10 +71,9 @@ public class HotspotIntroFragment extends Fragment { progressBar = v.findViewById(R.id.progressBar); progressTextView = v.findViewById(R.id.progressTextView); - startButton.setOnClickListener(button -> { - startButton.setEnabled(false); - conditionManager.startConditionChecks(); - }); + startButton.setOnClickListener(this::onButtonClick); + + conditionManager.init(requireActivity()); return v; } @@ -95,11 +81,15 @@ public class HotspotIntroFragment extends Fragment { @Override public void onStart() { super.onStart(); - conditionManager.resetPermissions(); + conditionManager.onStart(); + } + + private void onButtonClick(View view) { + startButton.setEnabled(false); + startHotspot(); } private void startHotspot() { - startButton.setEnabled(true); if (conditionManager.checkAndRequestConditions()) { showInstallWarningIfNeeded(); beginDelayedTransition((ViewGroup) requireView()); @@ -110,6 +100,13 @@ public class HotspotIntroFragment extends Fragment { } } + private void onPermissionUpdate(boolean recheckPermissions) { + startButton.setEnabled(true); + if (recheckPermissions) { + startHotspot(); + } + } + private void showInstallWarningIfNeeded() { Context ctx = requireContext(); ApplicationInfo applicationInfo; 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 index a27cd0851..4ecf5d911 100644 --- 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 @@ -54,7 +54,7 @@ import static org.briarproject.briar.android.util.UiUtils.handleException; @MethodsNotNullByDefault @ParametersNotNullByDefault -class HotspotManager implements ActionListener { +class HotspotManager { interface HotspotListener { void onStartingHotspot(); @@ -72,6 +72,7 @@ class HotspotManager implements ActionListener { private static final Logger LOG = getLogger(HotspotManager.class.getName()); + private static final int MAX_FRAMEWORK_ATTEMPTS = 5; private static final int MAX_GROUP_INFO_ATTEMPTS = 5; private static final int RETRY_DELAY_MILLIS = 1000; private static final String HOTSPOT_NAMESPACE = "hotspot"; @@ -133,12 +134,80 @@ class HotspotManager implements ActionListener { return; } listener.onStartingHotspot(); + acquireLocks(); + startWifiP2pFramework(1); + } + + /** + * As soon as Wifi is enabled, we try starting the WifiP2p framework. + * If Wifi has just been enabled, it is possible that will fail. If that + * happens we try again for MAX_FRAMEWORK_ATTEMPTS times after a delay of + * RETRY_DELAY_MILLIS after each attempt. + *

+ * Rationale: it can take a few milliseconds for WifiP2p to become available + * after enabling Wifi. Depending on the API level it is possible to check this + * using {@link WifiP2pManager#requestP2pState} or register a BroadcastReceiver + * on the WIFI_P2P_STATE_CHANGED_ACTION to get notified when WifiP2p is really + * available. Trying to implement a solution that works reliably using these + * checks turned out to be a long rabbit-hole with lots of corner cases and + * workarounds for specific situations. + * Instead we now rely on this trial-and-error approach of just starting + * the framework and retrying if it fails. + *

+ * We'll realize that the framework is busy when the ActionListener passed + * to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY) + */ + void startWifiP2pFramework(int attempt) { + if (LOG.isLoggable(INFO)) + LOG.info("startWifiP2pFramework attempt: " + attempt); + /* + * It is important that we call WifiP2pManager#initialize again + * for every attempt to starting the framework because otherwise, + * createGroup() will continue to fail with a BUSY state. + */ channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null); if (channel == null) { listener.onHotspotError( ctx.getString(R.string.hotspot_error_no_wifi_direct)); return; } + + ActionListener listener = new ActionListener() { + + @Override + // Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot() + public void onSuccess() { + requestGroupInfo(1); + } + + @Override + // Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot() + public void onFailure(int reason) { + LOG.info("onFailure: " + reason); + if (reason == BUSY) { + // WifiP2p not ready yet or hotspot already running + restartWifiP2pFramework(attempt); + } 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)); + } + } + }; + try { if (SDK_INT >= 29) { dbExecutor.execute(() -> { @@ -151,12 +220,12 @@ class HotspotManager implements ActionListener { .setPassphrase(savedNetworkConfig.password) .build(); acquireLocks(); - wifiP2pManager.createGroup(channel, config, this); + wifiP2pManager.createGroup(channel, config, listener); }); }); } else { acquireLocks(); - wifiP2pManager.createGroup(channel, this); + wifiP2pManager.createGroup(channel, listener); } } catch (SecurityException e) { // this should never happen, because we request permissions before @@ -164,37 +233,18 @@ class HotspotManager implements ActionListener { } } - @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")); + private void restartWifiP2pFramework(int attempt) { + LOG.info("retrying to start WifiP2p framework"); + if (attempt < MAX_FRAMEWORK_ATTEMPTS) { + handler.postDelayed(() -> startWifiP2pFramework(attempt + 1), + RETRY_DELAY_MILLIS); } else { - // all cases covered, in doubt set to error - releaseHotspotWithError(ctx.getString( - R.string.hotspot_error_start_callback_failed_unknown, - reason)); + releaseHotspotWithError( + ctx.getString(R.string.hotspot_error_framework_busy)); } } + @UiThread void stopWifiP2pHotspot() { if (channel == null) return; wifiP2pManager.removeGroup(channel, new ActionListener() { @@ -301,7 +351,7 @@ class HotspotManager implements ActionListener { } private void retryRequestingGroupInfo(int attempt) { - LOG.info("retrying"); + LOG.info("retrying to request group info"); // 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), 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 7b923e1e0..5bf44eaa7 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 @@ -110,7 +110,7 @@ class HotspotViewModel extends DbViewModel } @UiThread - private void stopHotspot() { + void stopHotspot() { ioExecutor.execute(webServerManager::stopWebServer); hotspotManager.stopWifiP2pHotspot(); notificationManager.clearHotspotNotification(); 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 2146bffd1..22486c4fe 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 @@ -6,6 +6,7 @@ import android.app.Activity; import android.app.KeyguardManager; import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Resources; @@ -52,6 +53,7 @@ import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; import androidx.annotation.UiThread; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; @@ -561,4 +563,31 @@ public class UiUtils { activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE | SOFT_INPUT_STATE_HIDDEN); } + + public static void showDenialDialog(FragmentActivity ctx, + @StringRes int title, + @StringRes int body, DialogInterface.OnClickListener onOkClicked, + Runnable onDismiss) { + 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.setOnDismissListener(dialog -> onDismiss.run()); + builder.show(); + } + + public static void showRationale(Context ctx, @StringRes int title, + @StringRes int body, + Runnable onContinueClicked, Runnable onDismiss) { + AlertDialog.Builder builder = new AlertDialog.Builder(ctx); + builder.setTitle(title); + builder.setMessage(body); + builder.setNeutralButton(R.string.continue_button, + (dialog, which) -> onContinueClicked.run()); + builder.setOnDismissListener(dialog -> onDismiss.run()); + builder.show(); + } + } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index c4198032d..4fbebca60 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -757,6 +757,7 @@ Error starting web server! Error presenting website.\n\nPlease send feedback (with anonymous data) via the Briar app if the issue persists. Warning: This app was installed with Android Studio and can NOT be installed on another device. + Unable to start the hotspot. If you have another hotspot running or are sharing your internet connection via Wifi, try stopping that and try again afterwards.