mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-20 14:49:53 +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;
|
package org.briarproject.briar.android.hotspot;
|
||||||
|
|
||||||
import android.content.DialogInterface.OnClickListener;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.provider.Settings;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import androidx.core.util.Consumer;
|
||||||
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.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
|
||||||
import static android.content.Context.WIFI_SERVICE;
|
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.
|
* Abstract base class for the ConditionManagers that ensure that the conditions
|
||||||
* <p>
|
* to open a hotspot are fulfilled. There are different extensions of this for
|
||||||
* Be sure to call {@link #onRequestPermissionResult(Boolean)} and
|
* API levels lower than 29 and 29+.
|
||||||
* {@link #onRequestWifiEnabledResult()} when you get the
|
|
||||||
* {@link ActivityResult}.
|
|
||||||
* <p>
|
|
||||||
* As soon as {@link #checkAndRequestConditions()} returns true,
|
|
||||||
* all conditions are fulfilled.
|
|
||||||
*/
|
*/
|
||||||
@NotNullByDefault
|
abstract class ConditionManager {
|
||||||
class ConditionManager {
|
|
||||||
|
|
||||||
private enum Permission {
|
enum Permission {
|
||||||
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
||||||
}
|
}
|
||||||
|
|
||||||
private Permission locationPermission = Permission.UNKNOWN;
|
protected final Consumer<Boolean> permissionUpdateCallback;
|
||||||
private Permission wifiSetting = Permission.SHOW_RATIONALE;
|
protected FragmentActivity ctx;
|
||||||
|
protected WifiManager wifiManager;
|
||||||
|
|
||||||
private final FragmentActivity ctx;
|
ConditionManager(Consumer<Boolean> permissionUpdateCallback) {
|
||||||
private final WifiManager wifiManager;
|
this.permissionUpdateCallback = permissionUpdateCallback;
|
||||||
private final ActivityResultLauncher<String> locationRequest;
|
}
|
||||||
private final ActivityResultLauncher<Intent> wifiRequest;
|
|
||||||
|
|
||||||
ConditionManager(FragmentActivity ctx,
|
/**
|
||||||
ActivityResultLauncher<String> locationRequest,
|
* Pass a FragmentActivity context here during `onCreateView()`.
|
||||||
ActivityResultLauncher<Intent> wifiRequest) {
|
*/
|
||||||
|
void init(FragmentActivity ctx) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.wifiManager = (WifiManager) ctx.getApplicationContext()
|
this.wifiManager = (WifiManager) ctx.getApplicationContext()
|
||||||
.getSystemService(WIFI_SERVICE);
|
.getSystemService(WIFI_SERVICE);
|
||||||
this.locationRequest = locationRequest;
|
|
||||||
this.wifiRequest = wifiRequest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this to reset state when UI starts,
|
* Call this during onStart() in the fragment where the ConditionManager
|
||||||
* because state might have changed.
|
* is used.
|
||||||
*/
|
*/
|
||||||
void resetPermissions() {
|
abstract void onStart();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 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.
|
* @return true if conditions are fulfilled and flow can continue.
|
||||||
*/
|
*/
|
||||||
boolean checkAndRequestConditions() {
|
abstract 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package org.briarproject.briar.android.hotspot;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -20,15 +19,13 @@ import org.briarproject.briar.R;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
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.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
|
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.INVISIBLE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
||||||
@@ -45,22 +42,14 @@ public class HotspotIntroFragment extends Fragment {
|
|||||||
ViewModelProvider.Factory viewModelFactory;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
private HotspotViewModel viewModel;
|
private HotspotViewModel viewModel;
|
||||||
private ConditionManager conditionManager;
|
|
||||||
|
|
||||||
private Button startButton;
|
private Button startButton;
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
private TextView progressTextView;
|
private TextView progressTextView;
|
||||||
|
|
||||||
private final ActivityResultLauncher<String> locationRequest =
|
private final ConditionManager conditionManager = SDK_INT < 29 ?
|
||||||
registerForActivityResult(new RequestPermission(), granted -> {
|
new ConditionManagerImpl(this, this::onPermissionUpdate) :
|
||||||
conditionManager.onRequestPermissionResult(granted);
|
new ConditionManager29Impl(this, this::onPermissionUpdate);
|
||||||
startHotspot();
|
|
||||||
});
|
|
||||||
private final ActivityResultLauncher<Intent> wifiRequest =
|
|
||||||
registerForActivityResult(new StartActivityForResult(), result -> {
|
|
||||||
conditionManager.onRequestWifiEnabledResult();
|
|
||||||
startHotspot();
|
|
||||||
});
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(Context context) {
|
||||||
@@ -69,8 +58,6 @@ public class HotspotIntroFragment extends Fragment {
|
|||||||
getAndroidComponent(activity).inject(this);
|
getAndroidComponent(activity).inject(this);
|
||||||
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
||||||
.get(HotspotViewModel.class);
|
.get(HotspotViewModel.class);
|
||||||
conditionManager =
|
|
||||||
new ConditionManager(activity, locationRequest, wifiRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -84,10 +71,9 @@ public class HotspotIntroFragment extends Fragment {
|
|||||||
progressBar = v.findViewById(R.id.progressBar);
|
progressBar = v.findViewById(R.id.progressBar);
|
||||||
progressTextView = v.findViewById(R.id.progressTextView);
|
progressTextView = v.findViewById(R.id.progressTextView);
|
||||||
|
|
||||||
startButton.setOnClickListener(button -> {
|
startButton.setOnClickListener(this::onButtonClick);
|
||||||
startButton.setEnabled(false);
|
|
||||||
conditionManager.startConditionChecks();
|
conditionManager.init(requireActivity());
|
||||||
});
|
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -95,11 +81,15 @@ public class HotspotIntroFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
conditionManager.resetPermissions();
|
conditionManager.onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onButtonClick(View view) {
|
||||||
|
startButton.setEnabled(false);
|
||||||
|
startHotspot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startHotspot() {
|
private void startHotspot() {
|
||||||
startButton.setEnabled(true);
|
|
||||||
if (conditionManager.checkAndRequestConditions()) {
|
if (conditionManager.checkAndRequestConditions()) {
|
||||||
showInstallWarningIfNeeded();
|
showInstallWarningIfNeeded();
|
||||||
beginDelayedTransition((ViewGroup) requireView());
|
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() {
|
private void showInstallWarningIfNeeded() {
|
||||||
Context ctx = requireContext();
|
Context ctx = requireContext();
|
||||||
ApplicationInfo applicationInfo;
|
ApplicationInfo applicationInfo;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import static org.briarproject.briar.android.util.UiUtils.handleException;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class HotspotManager implements ActionListener {
|
class HotspotManager {
|
||||||
|
|
||||||
interface HotspotListener {
|
interface HotspotListener {
|
||||||
void onStartingHotspot();
|
void onStartingHotspot();
|
||||||
@@ -72,6 +72,7 @@ class HotspotManager implements ActionListener {
|
|||||||
|
|
||||||
private static final Logger LOG = getLogger(HotspotManager.class.getName());
|
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 MAX_GROUP_INFO_ATTEMPTS = 5;
|
||||||
private static final int RETRY_DELAY_MILLIS = 1000;
|
private static final int RETRY_DELAY_MILLIS = 1000;
|
||||||
private static final String HOTSPOT_NAMESPACE = "hotspot";
|
private static final String HOTSPOT_NAMESPACE = "hotspot";
|
||||||
@@ -133,12 +134,80 @@ class HotspotManager implements ActionListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listener.onStartingHotspot();
|
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);
|
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
listener.onHotspotError(
|
listener.onHotspotError(
|
||||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||||
return;
|
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 {
|
try {
|
||||||
if (SDK_INT >= 29) {
|
if (SDK_INT >= 29) {
|
||||||
dbExecutor.execute(() -> {
|
dbExecutor.execute(() -> {
|
||||||
@@ -151,12 +220,12 @@ class HotspotManager implements ActionListener {
|
|||||||
.setPassphrase(savedNetworkConfig.password)
|
.setPassphrase(savedNetworkConfig.password)
|
||||||
.build();
|
.build();
|
||||||
acquireLocks();
|
acquireLocks();
|
||||||
wifiP2pManager.createGroup(channel, config, this);
|
wifiP2pManager.createGroup(channel, config, listener);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
acquireLocks();
|
acquireLocks();
|
||||||
wifiP2pManager.createGroup(channel, this);
|
wifiP2pManager.createGroup(channel, listener);
|
||||||
}
|
}
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
// this should never happen, because we request permissions before
|
// this should never happen, because we request permissions before
|
||||||
@@ -164,37 +233,18 @@ class HotspotManager implements ActionListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void restartWifiP2pFramework(int attempt) {
|
||||||
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
|
LOG.info("retrying to start WifiP2p framework");
|
||||||
public void onSuccess() {
|
if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
|
||||||
requestGroupInfo(1);
|
handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
|
||||||
}
|
RETRY_DELAY_MILLIS);
|
||||||
|
|
||||||
@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 {
|
} else {
|
||||||
// all cases covered, in doubt set to error
|
releaseHotspotWithError(
|
||||||
releaseHotspotWithError(ctx.getString(
|
ctx.getString(R.string.hotspot_error_framework_busy));
|
||||||
R.string.hotspot_error_start_callback_failed_unknown,
|
|
||||||
reason));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
void stopWifiP2pHotspot() {
|
void stopWifiP2pHotspot() {
|
||||||
if (channel == null) return;
|
if (channel == null) return;
|
||||||
wifiP2pManager.removeGroup(channel, new ActionListener() {
|
wifiP2pManager.removeGroup(channel, new ActionListener() {
|
||||||
@@ -301,7 +351,7 @@ class HotspotManager implements ActionListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void retryRequestingGroupInfo(int attempt) {
|
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
|
// On some devices we need to wait for the group info to become available
|
||||||
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
|
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
|
||||||
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
|
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class HotspotViewModel extends DbViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void stopHotspot() {
|
void stopHotspot() {
|
||||||
ioExecutor.execute(webServerManager::stopWebServer);
|
ioExecutor.execute(webServerManager::stopWebServer);
|
||||||
hotspotManager.stopWifiP2pHotspot();
|
hotspotManager.stopWifiP2pHotspot();
|
||||||
notificationManager.clearHotspotNotification();
|
notificationManager.clearHotspotNotification();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.app.Activity;
|
|||||||
import android.app.KeyguardManager;
|
import android.app.KeyguardManager;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -52,6 +53,7 @@ import androidx.annotation.ColorRes;
|
|||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
@@ -561,4 +563,31 @@ public class UiUtils {
|
|||||||
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
|
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
|
||||||
SOFT_INPUT_STATE_HIDDEN);
|
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_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_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_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 -->
|
<!-- Transfer Data via Removable Drives -->
|
||||||
|
|||||||
Reference in New Issue
Block a user