mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
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.
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* Be sure to call {@link #onRequestPermissionResult(Boolean)} and
|
||||
* {@link #onRequestWifiEnabledResult()} when you get the
|
||||
* {@link ActivityResult}.
|
||||
* <p>
|
||||
* 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<Boolean> permissionUpdateCallback;
|
||||
protected FragmentActivity ctx;
|
||||
protected WifiManager wifiManager;
|
||||
|
||||
private final FragmentActivity ctx;
|
||||
private final WifiManager wifiManager;
|
||||
private final ActivityResultLauncher<String> locationRequest;
|
||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
||||
ConditionManager(Consumer<Boolean> permissionUpdateCallback) {
|
||||
this.permissionUpdateCallback = permissionUpdateCallback;
|
||||
}
|
||||
|
||||
ConditionManager(FragmentActivity ctx,
|
||||
ActivityResultLauncher<String> locationRequest,
|
||||
ActivityResultLauncher<Intent> 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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<String> locationRequest;
|
||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
||||
|
||||
ConditionManager29Impl(ActivityResultCaller arc,
|
||||
Consumer<Boolean> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<Intent> wifiRequest;
|
||||
|
||||
ConditionManagerImpl(ActivityResultCaller arc,
|
||||
Consumer<Boolean> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> locationRequest =
|
||||
registerForActivityResult(new RequestPermission(), granted -> {
|
||||
conditionManager.onRequestPermissionResult(granted);
|
||||
startHotspot();
|
||||
});
|
||||
private final ActivityResultLauncher<Intent> 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;
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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),
|
||||
|
||||
@@ -110,7 +110,7 @@ class HotspotViewModel extends DbViewModel
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopHotspot() {
|
||||
void stopHotspot() {
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
hotspotManager.stopWifiP2pHotspot();
|
||||
notificationManager.clearHotspotNotification();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -757,6 +757,7 @@
|
||||
<string name="hotspot_error_web_server_start">Error starting web server!</string>
|
||||
<string name="hotspot_error_web_server_serve">Error presenting website.\n\nPlease send feedback (with anonymous data) via the Briar app if the issue persists.</string>
|
||||
<string name="hotspot_flag_test">Warning: This app was installed with Android Studio and can NOT be installed on another device.</string>
|
||||
<string name="hotspot_error_framework_busy">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.</string>
|
||||
|
||||
|
||||
<!-- Transfer Data via Removable Drives -->
|
||||
|
||||
Reference in New Issue
Block a user