diff --git a/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java b/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ec3e1f43fb6490fd4256f4f2514b66cbaa40e25b --- /dev/null +++ b/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java @@ -0,0 +1,53 @@ +package org.briarproject.hotspot; + +import android.net.wifi.WifiManager; + +import androidx.core.util.Consumer; +import androidx.fragment.app.FragmentActivity; + +import static android.content.Context.WIFI_SERVICE; + +/** + * 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+. + */ +abstract class AbstractConditionManager { + + enum Permission { + UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED + } + + protected final Consumer<Boolean> permissionUpdateCallback; + protected FragmentActivity ctx; + protected WifiManager wifiManager; + + AbstractConditionManager(Consumer<Boolean> permissionUpdateCallback) { + this.permissionUpdateCallback = permissionUpdateCallback; + } + + /** + * Pass a FragmentActivity context here during `onCreateView()`. + */ + void init(FragmentActivity ctx) { + this.ctx = ctx; + this.wifiManager = (WifiManager) ctx.getApplicationContext() + .getSystemService(WIFI_SERVICE); + } + + /** + * Call this during onStart() in the fragment where the ConditionManager + * is used. + */ + 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. + */ + abstract boolean checkAndRequestConditions(); + +} diff --git a/app/src/main/java/org/briarproject/hotspot/ConditionManager.java b/app/src/main/java/org/briarproject/hotspot/ConditionManager.java index 971fe5dbff92c7fd0c7d43e2d2d9ecc2007a1390..05d8988da62d93031aad9c9ce7e84920835f31c9 100644 --- a/app/src/main/java/org/briarproject/hotspot/ConditionManager.java +++ b/app/src/main/java/org/briarproject/hotspot/ConditionManager.java @@ -1,166 +1,82 @@ package org.briarproject.hotspot; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.net.wifi.WifiManager; import android.provider.Settings; -import androidx.activity.result.ActivityResult; +import java.util.logging.Logger; + +import androidx.activity.result.ActivityResultCaller; import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.FragmentActivity; +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; +import androidx.core.util.Consumer; -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.hotspot.UiUtils.getGoToSettingsListener; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.hotspot.UiUtils.showRationale; /** - * 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}. + * 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 ConditionManager { - - private enum Permission { - UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED - } +class ConditionManager extends AbstractConditionManager { - private Permission locationPermission = Permission.UNKNOWN; - private Permission wifiSetting = Permission.SHOW_RATIONALE; + private static final Logger LOG = + getLogger(ConditionManager.class.getName()); - private final FragmentActivity ctx; - private final WifiManager wifiManager; - private final ActivityResultLauncher<String> locationRequest; private final ActivityResultLauncher<Intent> wifiRequest; - ConditionManager(FragmentActivity ctx, - ActivityResultLauncher<String> locationRequest, - ActivityResultLauncher<Intent> wifiRequest) { - this.ctx = ctx; - this.wifiManager = (WifiManager) ctx.getApplicationContext() - .getSystemService(WIFI_SERVICE); - this.locationRequest = locationRequest; - this.wifiRequest = wifiRequest; + ConditionManager(ActivityResultCaller arc, + Consumer<Boolean> permissionUpdateCallback) { + super(permissionUpdateCallback); + wifiRequest = arc.registerForActivityResult( + new StartActivityForResult(), + result -> permissionUpdateCallback + .accept(wifiManager.isWifiEnabled())); } - /** - * Call this to reset state when UI starts, - * because state might have changed. - */ - void resetPermissions() { - locationPermission = Permission.UNKNOWN; - wifiSetting = Permission.SHOW_RATIONALE; + @Override + void onStart() { + // nothing to do here } - /** - * This makes a request for location permission. - * If {@link #checkAndRequestConditions()} returns true, you can continue. - */ - void startConditionChecks() { - locationRequest.launch(ACCESS_FINE_LOCATION); + private boolean areEssentialPermissionsGranted() { + if (LOG.isLoggable(INFO)) { + LOG.info(String.format("areEssentialPermissionsGranted(): " + + "wifiManager.isWifiEnabled()? %b", + wifiManager.isWifiEnabled())); + } + return wifiManager.isWifiEnabled(); } - /** - * @return true if conditions are fulfilled and flow can continue. - */ + @Override 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; - } + 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; + } - // 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, + // 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); - } - return false; - } - - void onRequestPermissionResult(Boolean granted) { - if (granted != null && granted) { - locationPermission = Permission.GRANTED; - } else if (shouldShowRequestPermissionRationale(ctx, - ACCESS_FINE_LOCATION)) { - locationPermission = Permission.SHOW_RATIONALE; - } else { - locationPermission = Permission.PERMANENTLY_DENIED; + this::requestEnableWiFi, + () -> permissionUpdateCallback.accept(false)); } - } - - 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); + return false; } private void requestEnableWiFi() { - Intent i = SDK_INT < 29 ? - new Intent(Settings.ACTION_WIFI_SETTINGS) : - new Intent(Settings.Panel.ACTION_WIFI); - wifiRequest.launch(i); + wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS)); } } diff --git a/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java b/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java new file mode 100644 index 0000000000000000000000000000000000000000..78888bc5f22f1be77ba9612736ee8a7f537c738c --- /dev/null +++ b/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java @@ -0,0 +1,136 @@ +package org.briarproject.hotspot; + +import android.content.Intent; +import android.provider.Settings; + +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.lang.Boolean.TRUE; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.hotspot.UiUtils.getGoToSettingsListener; +import static org.briarproject.hotspot.UiUtils.showDenialDialog; +import static org.briarproject.hotspot.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 ConditionManager29 extends AbstractConditionManager { + + private static final Logger LOG = + getLogger(ConditionManager29.class.getName()); + + private Permission locationPermission = Permission.UNKNOWN; + + private final ActivityResultLauncher<String> locationRequest; + private final ActivityResultLauncher<Intent> wifiRequest; + + ConditionManager29(ActivityResultCaller arc, + Consumer<Boolean> permissionUpdateCallback) { + super(permissionUpdateCallback); + locationRequest = arc.registerForActivityResult( + new RequestPermission(), granted -> { + onRequestPermissionResult(granted); + permissionUpdateCallback.accept(TRUE.equals(granted)); + }); + wifiRequest = arc.registerForActivityResult( + new StartActivityForResult(), + result -> permissionUpdateCallback + .accept(wifiManager.isWifiEnabled())); + } + + @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/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java b/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java index b608c84ec246a334d5c397f9117e088ded8d31db..f0aad6f16ee50cab492475c98d38cb6f2e7ce670 100644 --- a/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java +++ b/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java @@ -1,6 +1,5 @@ package org.briarproject.hotspot; -import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.view.LayoutInflater; @@ -16,14 +15,12 @@ import org.briarproject.hotspot.HotspotState.HotspotStopped; import org.briarproject.hotspot.HotspotState.NetworkConfig; import org.briarproject.hotspot.HotspotState.StartingHotspot; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import static android.os.Build.VERSION.SDK_INT; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static org.briarproject.hotspot.HotspotManager.UNKNOWN_FREQUENCY; @@ -33,30 +30,21 @@ import static org.briarproject.hotspot.QrCodeUtils.createWifiLoginString; public class HotspotFragment extends Fragment { private MainViewModel viewModel; - private ConditionManager conditionManager; private ImageView qrCode; private TextView ssidView, passwordView, statusView; private Button button, serverButton; private boolean hotspotStarted = false; - private final ActivityResultLauncher<String> locationRequest = - registerForActivityResult(new RequestPermission(), granted -> { - conditionManager.onRequestPermissionResult(granted); - startWifiP2pHotspot(); - }); - private final ActivityResultLauncher<Intent> wifiRequest = - registerForActivityResult(new StartActivityForResult(), result -> { - conditionManager.onRequestWifiEnabledResult(); - startWifiP2pHotspot(); - }); + private final AbstractConditionManager conditionManager = SDK_INT < 29 ? + new ConditionManager(this, this::onPermissionUpdate) : + new ConditionManager29(this, this::onPermissionUpdate); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { viewModel = new ViewModelProvider(requireActivity()) .get(MainViewModel.class); - conditionManager = new ConditionManager(requireActivity(), - locationRequest, wifiRequest); + conditionManager.init(requireActivity()); return inflater.inflate(R.layout.fragment_hotspot, container, false); } @@ -145,25 +133,33 @@ public class HotspotFragment extends Fragment { @Override public void onStart() { super.onStart(); - conditionManager.resetPermissions(); + conditionManager.onStart(); } - public void onButtonClick(View view) { + private void onButtonClick(View view) { + button.setEnabled(false); if (hotspotStarted) { - button.setEnabled(false); + // the hotspot is currently started → stop it viewModel.stopWifiP2pHotspot(); } else { - conditionManager.startConditionChecks(); + // the hotspot is currently stopped → start it + startWifiP2pHotspotIfConditionsFulfilled(); } } - private void startWifiP2pHotspot() { + private void startWifiP2pHotspotIfConditionsFulfilled() { if (conditionManager.checkAndRequestConditions()) { - button.setEnabled(false); viewModel.startWifiP2pHotspot(); } } + private void onPermissionUpdate(boolean recheckPermissions) { + button.setEnabled(true); + if (recheckPermissions) { + startWifiP2pHotspotIfConditionsFulfilled(); + } + } + public void onServerButtonClick(View view) { getParentFragmentManager().beginTransaction() .replace(R.id.fragment_container, new ServerFragment()) diff --git a/app/src/main/java/org/briarproject/hotspot/HotspotManager.java b/app/src/main/java/org/briarproject/hotspot/HotspotManager.java index 187d384177ffd49f1ce68024c82f91b2e94d1081..b59214f721096c01e2d2d2802cd8f46fe54d3176 100644 --- a/app/src/main/java/org/briarproject/hotspot/HotspotManager.java +++ b/app/src/main/java/org/briarproject/hotspot/HotspotManager.java @@ -30,7 +30,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Logger.getLogger; import static org.briarproject.hotspot.StringUtils.getRandomString; -class HotspotManager implements ActionListener { +class HotspotManager { interface HotspotListener { @@ -48,6 +48,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; @@ -85,26 +86,91 @@ class HotspotManager implements ActionListener { return; } listener.onStartingHotspot(); + acquireLock(); + 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.no_wifi_direct)); return; } - acquireLock(); + + 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.start_callback_failed, "p2p unsupported")); + } else if (reason == ERROR) { + releaseHotspotWithError(ctx.getString( + R.string.start_callback_failed, "p2p error")); + } else if (reason == NO_SERVICE_REQUESTS) { + releaseHotspotWithError(ctx.getString( + R.string.start_callback_failed, + "no service requests")); + } else { + // all cases covered, in doubt set to error + releaseHotspotWithError(ctx.getString( + R.string.start_callback_failed_unknown, reason)); + } + } + }; + try { if (SDK_INT >= 29) { networkName = getNetworkName(); String passphrase = getPassphrase(); // TODO: maybe remove this in the production version - LOG.info("networkName: " + networkName); + if (LOG.isLoggable(INFO)) + LOG.info("networkName: " + networkName); WifiP2pConfig config = new WifiP2pConfig.Builder() .setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ) .setNetworkName(networkName) .setPassphrase(passphrase) .build(); - wifiP2pManager.createGroup(channel, config, this); + wifiP2pManager.createGroup(channel, config, listener); } else { - wifiP2pManager.createGroup(channel, this); + wifiP2pManager.createGroup(channel, listener); } } catch (SecurityException e) { // this should never happen, because we request permissions before @@ -112,6 +178,17 @@ class HotspotManager implements ActionListener { } } + 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 { + releaseHotspotWithError( + ctx.getString(R.string.stop_framework_busy)); + } + } + @RequiresApi(29) private String getNetworkName() { return "DIRECT-" + getRandomString(2) + "-" + @@ -122,6 +199,7 @@ class HotspotManager implements ActionListener { return getRandomString(8); } + @UiThread void stopWifiP2pHotspot() { if (channel == null) return; wifiP2pManager.removeGroup(channel, new ActionListener() { @@ -164,33 +242,6 @@ class HotspotManager implements ActionListener { wifiLock.release(); } - @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.start_callback_failed, "p2p unsupported")); - else if (reason == ERROR) - releaseHotspotWithError(ctx.getString( - R.string.start_callback_failed, "p2p error")); - else if (reason == NO_SERVICE_REQUESTS) - releaseHotspotWithError(ctx.getString( - R.string.start_callback_failed, "no service requests")); - else - // all cases covered, in doubt set to error - releaseHotspotWithError(ctx.getString( - R.string.start_callback_failed_unknown, reason)); - } - private void requestGroupInfo(int attempt) { if (LOG.isLoggable(INFO)) LOG.info("requestGroupInfo attempt: " + attempt); @@ -279,7 +330,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/app/src/main/java/org/briarproject/hotspot/UiUtils.java b/app/src/main/java/org/briarproject/hotspot/UiUtils.java index 84959e353b532cbc6e797db52acaecf96e778183..551aa181e44c8749576e37e06e29ba7d97739bef 100644 --- a/app/src/main/java/org/briarproject/hotspot/UiUtils.java +++ b/app/src/main/java/org/briarproject/hotspot/UiUtils.java @@ -5,13 +5,17 @@ import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + import static android.content.Intent.CATEGORY_DEFAULT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static org.briarproject.hotspot.BuildConfig.APPLICATION_ID; class UiUtils { - public static DialogInterface.OnClickListener getGoToSettingsListener( + static DialogInterface.OnClickListener getGoToSettingsListener( Context context) { return (dialog, which) -> { Intent i = new Intent(); @@ -23,4 +27,29 @@ class UiUtils { }; } + 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(); + } + + 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/app/src/main/res/layout/fragment_hotspot.xml b/app/src/main/res/layout/fragment_hotspot.xml index 24dbe332043b5dda19a5e5db60b2d514dca2e9af..654603e44cd30c55f1f2237867b64ce86069f2c2 100644 --- a/app/src/main/res/layout/fragment_hotspot.xml +++ b/app/src/main/res/layout/fragment_hotspot.xml @@ -45,7 +45,9 @@ android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="16sp" /> + android:layout_margin="16dp" + android:textSize="16sp" + tools:text="@string/stop_framework_busy" /> <Button android:id="@+id/serverButton" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2fdb737657919f89e7aeca61075ec90578cef3f..8db67ce71501a876ffe90968d2ac3614df54ffbf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ <string name="start_callback_no_group_info">Hotspot failed to start: no group info</string> <string name="start_no_attempts_left">Hotspot failed to start with an unknown error</string> <string name="stop_callback_failed">Unknown error while stopping hotspot (reason %d)</string> + <string name="stop_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> <string name="hotspot_stopped">Hotspot stopped</string> <string name="qr_code_description">QR code with Wi-Fi login details</string> <string name="no_wifi_manager">Device does not support Wi-Fi</string>