Address review feedback for feature branch

This commit is contained in:
Torsten Grote
2021-07-29 15:35:46 +02:00
parent e9dbceefe8
commit acacb59114
15 changed files with 108 additions and 124 deletions

View File

@@ -92,9 +92,9 @@
<li id="troubleshooting_1">If you can't download the app, try it with a different web
browser app.
</li>
<li id="troubleshooting_2">Ensure that your browser is allowed to download apps directly by
giving it the permission or enabling the installation of apps from "Unknown Sources" in
system settings.
<li id="troubleshooting_2">To install the downloaded app,
you might need to allow your browser to install unknown apps.
We recommend to undo that after successful installation.
</li>
</ol>
</div>

View File

@@ -61,15 +61,14 @@ class ConditionManager29 extends AbstractConditionManager {
}
private boolean areEssentialPermissionsGranted() {
boolean isWifiEnabled = wifiManager.isWifiEnabled();
if (LOG.isLoggable(INFO)) {
LOG.info(String.format("areEssentialPermissionsGranted(): " +
"locationPermission? %s, " +
"wifiManager.isWifiEnabled()? %b",
locationPermission,
wifiManager.isWifiEnabled()));
locationPermission, isWifiEnabled));
}
return locationPermission == Permission.GRANTED &&
wifiManager.isWifiEnabled();
return locationPermission == Permission.GRANTED && isWifiEnabled;
}
@Override

View File

@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced;
import java.util.List;
@@ -22,7 +23,6 @@ import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
@@ -33,12 +33,10 @@ import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.activity.result.contract.ActivityResultContracts.CreateDocument;
import static androidx.transition.TransitionManager.beginDelayedTransition;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FallbackFragment extends BaseFragment {
@@ -50,7 +48,7 @@ public class FallbackFragment extends BaseFragment {
private HotspotViewModel viewModel;
private final ActivityResultLauncher<String> launcher =
registerForActivityResult(new CreateDocument(),
registerForActivityResult(new CreateDocumentAdvanced(),
this::onDocumentCreated);
private Button fallbackButton;
private ProgressBar progressBar;
@@ -75,7 +73,7 @@ public class FallbackFragment extends BaseFragment {
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater
.inflate(R.layout.fragment_hotspot_save_apk, container, false);
.inflate(R.layout.fragment_hotspot_fallback, container, false);
}
@Override
@@ -92,8 +90,7 @@ public class FallbackFragment extends BaseFragment {
if (SDK_INT >= 19) launcher.launch(getApkFileName());
else viewModel.exportApk();
});
viewModel.getSavedApkToUri()
.observeEvent(this, uri -> shareUri(this, uri));
viewModel.getSavedApkToUri().observeEvent(this, this::shareUri);
}
private void onDocumentCreated(@Nullable Uri uri) {
@@ -107,12 +104,12 @@ public class FallbackFragment extends BaseFragment {
progressBar.setVisibility(INVISIBLE);
}
static void shareUri(Fragment fragment, Uri uri) {
void shareUri(Uri uri) {
Intent i = new Intent(ACTION_SEND);
i.putExtra(EXTRA_STREAM, uri);
i.setType("*/*"); // gives us all sharing options
i.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
Context ctx = fragment.requireContext();
Context ctx = requireContext();
if (SDK_INT <= 19) {
// Workaround for Android bug:
// ctx.grantUriPermission also needed for Android 4
@@ -124,7 +121,7 @@ public class FallbackFragment extends BaseFragment {
FLAG_GRANT_READ_URI_PERMISSION);
}
}
fragment.startActivity(Intent.createChooser(i, null));
startActivity(Intent.createChooser(i, null));
}
}

View File

@@ -59,12 +59,12 @@ public class HotspotActivity extends BriarActivity
// check if fragment is already added
// to not lose state on configuration changes
if (fm.findFragmentByTag(tag) == null) {
if (!started.consume()) {
if (started.wasNotYetConsumed()) {
showFragment(fm, new HotspotFragment(), tag);
}
}
} else if (hotspotState instanceof HotspotError) {
HotspotError error = ((HotspotError) hotspotState);
HotspotError error = (HotspotError) hotspotState;
showErrorFragment(error.getError());
}
});

View File

@@ -2,13 +2,11 @@ package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.View;
import com.google.android.material.snackbar.Snackbar;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -41,13 +39,8 @@ public class HotspotFragment extends AbstractTabsFragment {
private void onPeerConnected(boolean connected) {
if (!connected) return;
new BriarSnackbarBuilder()
.setAction(R.string.hotspot_peer_connected_action, v ->
showNextFragment())
.make(connectedButton, R.string.hotspot_peer_connected,
Snackbar.LENGTH_LONG)
.setAnchorView(connectedButton)
.show();
Toast.makeText(requireContext(), R.string.hotspot_peer_connected,
Toast.LENGTH_LONG).show();
}
private void showNextFragment() {

View File

@@ -48,7 +48,9 @@ import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.FULL_WAKE_LOCK;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.util.UiUtils.handleException;
@@ -57,6 +59,7 @@ import static org.briarproject.briar.android.util.UiUtils.handleException;
class HotspotManager {
interface HotspotListener {
@UiThread
void onStartingHotspot();
@IoExecutor
@@ -65,8 +68,7 @@ class HotspotManager {
@UiThread
void onDeviceConnected();
void onHotspotStopped();
@UiThread
void onHotspotError(String error);
}
@@ -97,8 +99,9 @@ class HotspotManager {
private WifiManager.WifiLock wifiLock;
private PowerManager.WakeLock wakeLock;
private WifiP2pManager.Channel channel;
@Nullable
@RequiresApi(29)
private volatile NetworkConfig savedNetworkConfig;
private volatile NetworkConfig savedNetworkConfig = null;
@Inject
HotspotManager(Application ctx,
@@ -157,9 +160,11 @@ class HotspotManager {
* We'll realize that the framework is busy when the ActionListener passed
* to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY)
*/
@UiThread
private void startWifiP2pFramework(int attempt) {
if (LOG.isLoggable(INFO))
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,
@@ -167,13 +172,12 @@ class HotspotManager {
*/
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
if (channel == null) {
listener.onHotspotError(
releaseHotspotWithError(
ctx.getString(R.string.hotspot_error_no_wifi_direct));
return;
}
ActionListener listener = new ActionListener() {
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onSuccess() {
@@ -183,7 +187,9 @@ class HotspotManager {
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onFailure(int reason) {
LOG.info("onFailure: " + reason);
if (LOG.isLoggable(INFO)) {
LOG.info("onFailure: " + reason);
}
if (reason == BUSY) {
// WifiP2p not ready yet or hotspot already running
restartWifiP2pFramework(attempt);
@@ -210,21 +216,26 @@ class HotspotManager {
try {
if (SDK_INT >= 29) {
dbExecutor.execute(() -> {
Runnable createGroup = () -> {
NetworkConfig c = requireNonNull(savedNetworkConfig);
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(c.ssid)
.setPassphrase(c.password)
.build();
wifiP2pManager.createGroup(channel, config, listener);
};
if (savedNetworkConfig == null) {
// load savedNetworkConfig before starting hotspot
loadSavedNetworkConfig();
androidExecutor.runOnUiThread(() -> {
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(savedNetworkConfig.ssid)
.setPassphrase(savedNetworkConfig.password)
.build();
acquireLocks();
wifiP2pManager.createGroup(channel, config, listener);
dbExecutor.execute(() -> {
loadSavedNetworkConfig();
androidExecutor.runOnUiThread(createGroup);
});
});
} else {
// savedNetworkConfig was already loaded, create group now
createGroup.run();
}
} else {
acquireLocks();
wifiP2pManager.createGroup(channel, listener);
}
} catch (SecurityException e) {
@@ -233,9 +244,11 @@ class HotspotManager {
}
}
@UiThread
private void restartWifiP2pFramework(int attempt) {
LOG.info("retrying to start WifiP2p framework");
if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
if (SDK_INT >= 27 && channel != null) channel.close();
handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
@@ -250,19 +263,23 @@ class HotspotManager {
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onSuccess() {
releaseHotspot();
closeChannelAndReleaseLocks();
}
@Override
public void onFailure(int reason) {
// not propagating back error
releaseHotspot();
if (LOG.isLoggable(WARNING)) {
LOG.warning("Error removing Wifi P2P group: " + reason);
}
closeChannelAndReleaseLocks();
}
});
}
@SuppressLint("WakelockTimeout")
private void acquireLocks() {
// FLAG_KEEP_SCREEN_ON is not respected on some Huawei devices.
wakeLock = powerManager.newWakeLock(FULL_WAKE_LOCK, lockTag);
wakeLock.acquire();
// WIFI_MODE_FULL has no effect on API >= 29
@@ -272,23 +289,21 @@ class HotspotManager {
wifiLock.acquire();
}
private void releaseHotspot() {
listener.onHotspotStopped();
closeChannelAndReleaseLocks();
}
@UiThread
private void releaseHotspotWithError(String error) {
listener.onHotspotError(error);
closeChannelAndReleaseLocks();
}
@UiThread
private void closeChannelAndReleaseLocks() {
if (SDK_INT >= 27) channel.close();
if (SDK_INT >= 27 && channel != null) channel.close();
channel = null;
wakeLock.release();
wifiLock.release();
if (wakeLock.isHeld()) wakeLock.release();
if (wifiLock.isHeld()) wifiLock.release();
}
@UiThread
private void requestGroupInfo(int attempt) {
if (LOG.isLoggable(INFO)) {
LOG.info("requestGroupInfo attempt: " + attempt);
@@ -331,25 +346,20 @@ class HotspotManager {
LOG.info("group is null");
return false;
} else if (!group.getNetworkName().startsWith("DIRECT-")) {
if (LOG.isLoggable(INFO)) {
LOG.info("received networkName without prefix 'DIRECT-': " +
group.getNetworkName());
}
LOG.info("received networkName without prefix 'DIRECT-'");
return false;
} else if (SDK_INT >= 29) {
// if we get here, the savedNetworkConfig must have a value
String networkName = savedNetworkConfig.ssid;
String networkName = requireNonNull(savedNetworkConfig).ssid;
if (!networkName.equals(group.getNetworkName())) {
if (LOG.isLoggable(INFO)) {
LOG.info("expected networkName: " + networkName);
LOG.info("received networkName: " + group.getNetworkName());
}
LOG.info("expected networkName does not match received one");
return false;
}
}
return true;
}
@UiThread
private void retryRequestingGroupInfo(int attempt) {
LOG.info("retrying to request group info");
// On some devices we need to wait for the group info to become available
@@ -364,17 +374,12 @@ class HotspotManager {
@UiThread
private void requestGroupInfoForConnection() {
if (LOG.isLoggable(INFO)) {
LOG.info("requestGroupInfo for connection");
}
LOG.info("requestGroupInfo for connection");
GroupInfoListener groupListener = group -> {
if (group == null || group.getClientList().isEmpty()) {
handler.postDelayed(this::requestGroupInfoForConnection,
RETRY_DELAY_MILLIS);
} else {
if (LOG.isLoggable(INFO)) {
LOG.info("client list " + group.getClientList());
}
listener.onDeviceConnected();
}
};
@@ -433,26 +438,15 @@ class HotspotManager {
}
// exclude chars that are easy to confuse: 0 O, 5 S, 1 l I
private static final String digits = "2346789";
private static final String letters = "abcdefghijkmnopqrstuvwxyz";
private static final String LETTERS = "ABCDEFGHJKLMNPQRTUVWXYZ";
private static final String chars =
"2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnopqrstuvwxyz";
private String getRandomString(int length) {
char[] c = new char[length];
for (int i = 0; i < length; i++) {
if (random.nextBoolean()) {
c[i] = random(digits);
} else if (random.nextBoolean()) {
c[i] = random(letters);
} else {
c[i] = random(LETTERS);
}
c[i] = chars.charAt(random.nextInt(chars.length()));
}
return new String(c);
}
private char random(String universe) {
return universe.charAt(random.nextInt(universe.length()));
}
}

View File

@@ -63,10 +63,10 @@ abstract class HotspotState {
* to not repeat actions such as showing fragments on rotation changes.
*/
@UiThread
boolean consume() {
boolean wasNotYetConsumed() {
boolean old = consumed;
consumed = true;
return old;
return !old;
}
}

View File

@@ -141,12 +141,6 @@ class HotspotViewModel extends DbViewModel
peerConnected.setEvent(true);
}
@Override
public void onHotspotStopped() {
LOG.info("stopping webserver");
ioExecutor.execute(webServerManager::stopWebServer);
}
@Override
public void onHotspotError(String error) {
if (LOG.isLoggable(WARNING)) {
@@ -170,7 +164,7 @@ class HotspotViewModel extends DbViewModel
public void onWebServerError() {
state.postValue(new HotspotError(getApplication()
.getString(R.string.hotspot_error_web_server_start)));
hotspotManager.stopWifiP2pHotspot();
stopHotspot();
}
void exportApk(Uri uri) {

View File

@@ -1,12 +1,14 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -16,7 +18,6 @@ import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -60,19 +61,23 @@ public class QrHotspotFragment extends Fragment {
TextView qrIntroView = v.findViewById(R.id.qrIntroView);
ImageView qrCodeView = v.findViewById(R.id.qrCodeView);
Consumer<HotspotStarted> consumer;
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
qrIntroView.setText(R.string.hotspot_qr_wifi);
consumer = state ->
qrCodeView.setImageBitmap(state.getNetworkConfig().qrCode);
} else {
qrIntroView.setText(R.string.hotspot_qr_site);
consumer = state ->
qrCodeView.setImageBitmap(state.getWebsiteConfig().qrCode);
}
boolean forWifi = requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT);
qrIntroView.setText(forWifi ? R.string.hotspot_qr_wifi :
R.string.hotspot_qr_site);
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
if (state instanceof HotspotStarted) {
consumer.accept((HotspotStarted) state);
HotspotStarted s = (HotspotStarted) state;
Bitmap qrCode = forWifi ? s.getNetworkConfig().qrCode :
s.getWebsiteConfig().qrCode;
if (qrCode == null) {
Toast.makeText(requireContext(), R.string.error,
Toast.LENGTH_SHORT).show();
qrCodeView.setImageResource(R.drawable.ic_image_broken);
} else {
qrCodeView.setImageBitmap(qrCode);
}
}
});
return v;

View File

@@ -22,6 +22,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
@@ -49,7 +50,7 @@ class WebServerManager {
private final WebServer webServer;
private final DisplayMetrics dm;
private WebServerListener listener;
private volatile WebServerListener listener;
@Inject
WebServerManager(Application ctx) {
@@ -57,6 +58,7 @@ class WebServerManager {
dm = ctx.getResources().getDisplayMetrics();
}
@UiThread
void setListener(WebServerListener listener) {
this.listener = listener;
}

View File

@@ -73,7 +73,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/feedbackButton"
tools:layout="@layout/fragment_hotspot_save_apk" />
tools:layout="@layout/fragment_hotspot_fallback" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -8,7 +8,7 @@
android:id="@id/fallbackTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginTop="16dp"
android:text="@string/hotspot_help_fallback_title"
android:textSize="16sp"
android:textStyle="bold"

View File

@@ -119,7 +119,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/site4View"
tools:layout="@layout/fragment_hotspot_save_apk" />
tools:layout="@layout/fragment_hotspot_fallback" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -22,7 +22,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside"
app:srcCompat="@drawable/ic_nickname"
app:srcCompat="@drawable/ic_wifi_tethering"
tools:ignore="ContentDescription" />
<TextView

View File

@@ -694,7 +694,7 @@
<!-- Share app offline -->
<string name="hotspot_title">Share Briar offline</string>
<string name="hotspot_intro">Share this app with someone nearby without internet connection by using your phone\'s Wi-Fi.
<string name="hotspot_intro">Share this app with someone nearby without Internet connection by using your phone\'s Wi-Fi.
\n\nYour phone will start a Wi-Fi hotspot. People nearby can connect to the hotspot and download the Briar app from your phone.</string>
<string name="hotspot_button_start_sharing">Start hotspot</string>
<string name="hotspot_button_stop_sharing">Stop hotspot</string>
@@ -707,9 +707,10 @@
<string name="permission_hotspot_location_denied_body">You have denied access to your location, but Briar needs this permission to create a Wi-Fi hotspot.\n\nPlease consider granting access.</string>
<string name="wifi_settings_title">Wi-Fi setting</string>
<string name="wifi_settings_request_enable_body">To create a Wi-Fi hotspot, Briar needs to use Wi-Fi. Please enable it.</string>
<string name="wifi_settings_request_denied_body">You have denied to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.</string>
<string name="wifi_settings_request_denied_body">You have denied permission to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.</string>
<string name="hotspot_tab_manual">Manual</string>
<!-- The placeholder to be inserted into the string 'hotspot_manual_wifi': People can connect by %s -->
<string name="hotspot_scanning_a_qr_code">scanning a QR code</string>
<!-- Wi-Fi setup -->
@@ -718,7 +719,6 @@
<string name="hotspot_manual_wifi_ssid">Network name (SSID)</string>
<string name="hotspot_qr_wifi">Your phone is providing a Wi-Fi hotspot. People who want to download Briar can connect to the hotspot by scanning this QR code. When they have connected to the hotspot, press \'Next\'.</string>
<string name="hotspot_peer_connected">Successfully connected</string>
<string name="hotspot_peer_connected_action">Show download info</string>
<!-- Download link -->
<!-- The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code' -->
@@ -732,21 +732,21 @@
<string name="website_download_outro">After the download is complete, open the downloaded file and install it.</string>
<string name="website_troubleshooting_title">Troubleshooting</string>
<string name="website_troubleshooting_1">If you cannot download the app, try it with a different web browser app.</string>
<string name="website_troubleshooting_2_old">To install the downloaded app, you might need to allow installation of apps from \"Unknown sources\" in system settings. Afterwards, you may need to download the app again. We recommend to undo that after successful installation.</string>
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser to install unknown apps. We recommend to undo that after successful installation.</string>
<string name="website_troubleshooting_2_old">To install the downloaded app, you might need to allow installation of apps from \"Unknown sources\" in system settings. Afterwards, you may need to download the app again. We recommend disabling the \"Unknown sources\" setting after installing the app.</string>
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser to install unknown apps. After installing the app, we recommend removing the browser\'s permission to install unknown apps.</string>
<string name="hotspot_help_wifi_title">Problems with connecting to Wi-Fi:</string>
<string name="hotspot_help_wifi_1">Try disabling and re-enabling Wi-Fi on both phones and try again.</string>
<string name="hotspot_help_wifi_2">If your phone complains that the Wi-Fi has no internet, tell it that you want to stay connected anyway.</string>
<string name="hotspot_help_wifi_2">If your phone complains that the Wi-Fi has no Internet, tell it that you want to stay connected anyway.</string>
<string name="hotspot_help_site_title">Problems visiting the local website:</string>
<string name="hotspot_help_site_1">Double check that you entered the address exactly as shown. A small error can make it fail.</string>
<string name="hotspot_help_site_2">Ensure that your phone is still connected to the correct Wi-Fi (see above) when you try to access the site.</string>
<string name="hotspot_help_site_3">Check that you don\'t have any active firewall apps that may block the access.</string>
<string name="hotspot_help_site_3">If you have a firewall app, check that it isn\'t blocking access.</string>
<string name="hotspot_help_site_4">If you can visit the site, but not download the Briar app, try it with a different web browser app.</string>
<string name="hotspot_help_fallback_title">Nothing works?</string>
<string name="hotspot_help_fallback_intro">You can try to save the app as an .apk file to share in some other way. Once on the other device, it can be used to install Briar.
<string name="hotspot_help_fallback_intro">You can try to save the app as an .apk file to share in some other way. Once the file has been transferred to the other device, it can be used to install Briar.
\n\nTip: For sharing via Bluetooth, you might need to rename the file to end with .zip first.</string>
<string name="hotspot_help_fallback_button">Save app install file</string>
<string name="hotspot_help_fallback_button">Save app</string>
<!-- error handling -->
<string name="hotspot_error_intro">Something went wrong while trying to share the app via Wi-Fi:</string>
@@ -754,10 +754,10 @@
<string name="hotspot_error_start_callback_failed">Hotspot failed to start: error %s</string>
<string name="hotspot_error_start_callback_failed_unknown">Hotspot failed to start with an unknown error, reason %d</string>
<string name="hotspot_error_start_callback_no_group_info">Hotspot failed to start: no group info</string>
<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_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.\n\nIf you have another hotspot running or are sharing your internet connection via Wi-Fi, try stopping that and try again afterwards.</string>
<string name="hotspot_error_framework_busy">Unable to start the hotspot.\n\nIf you have another hotspot running or are sharing your Internet connection via Wi-Fi, try stopping that and try again afterwards.</string>
<!-- Transfer Data via Removable Drives -->