mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Port code from Offline hotspot test app
This commit is contained in:
committed by
Sebastian Kürten
parent
15f5c8deee
commit
99da50d37c
@@ -121,6 +121,7 @@ dependencies {
|
||||
exclude group: 'com.android.support'
|
||||
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
|
||||
}
|
||||
implementation 'org.nanohttpd:nanohttpd:2.3.1'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
103
briar-android/src/main/assets/hotspot.html
Normal file
103
briar-android/src/main/assets/hotspot.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
background-color: #F2F2F2;
|
||||
font-family: Roboto,Arial,Helvetica,sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
div#top {
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
}
|
||||
div#bottom {
|
||||
padding: 16px 32px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
a.button {
|
||||
background-color: #82C91E;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 32px !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
color: #000000 !important;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
margin: 20px auto 20px auto;
|
||||
}
|
||||
ol {
|
||||
list-style: none;
|
||||
counter-reset: briar-counter;
|
||||
padding-left: 40px;
|
||||
}
|
||||
ol li {
|
||||
counter-increment: briar-counter;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
ol li::before {
|
||||
content: counter(briar-counter);
|
||||
background-color: #82C91E;
|
||||
color: #000000 !important;
|
||||
font-weight: bold;
|
||||
border-radius: 70px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="top">
|
||||
<svg style="width:156px;height:47px;" viewBox="0 0 778 235">
|
||||
<path style="fill:#87c214"
|
||||
d="m 64.900391,0 c -9.7,0 -17.701172,7.9992183 -17.701172,17.699219 v 22.5 h 43.601562 v -22.5 C 90.800781,7.9992183 82.899219,0 73.199219,0 Z m 96.999999,0 c -9.7,0 -17.70117,7.9992183 -17.70117,17.699219 V 137.19922 h 43.60156 V 17.699219 C 187.80078,7.9992183 179.89922,0 170.19922,0 Z M 47.199219,97.800781 V 217.30078 c 0,9.7 7.901172,17.69922 17.701172,17.69922 h 8.298828 c 9.7,0 17.701172,-7.99922 17.701172,-17.69922 V 97.800781 Z m 97.000001,96.999999 v 22.5 c 0,9.7 8.00117,17.69922 17.70117,17.69922 h 8.29883 c 9.7,0 17.70117,-7.99922 17.70117,-17.69922 v -22.5 z"/>
|
||||
<path style="fill:#95d220"
|
||||
d="M 17.699219,47.199219 C 7.9992186,47.199219 0,55.100391 0,64.900391 v 8.298828 c 0,9.7 7.8992186,17.701172 17.699219,17.701172 H 137.19922 V 47.199219 Z m 177.101561,0 v 43.701172 h 22.5 c 9.7,0 17.69922,-7.901172 17.69922,-17.701172 v -8.298828 c 0,-9.8 -7.99922,-17.701172 -17.69922,-17.701172 z M 17.699219,144.19922 C 7.9992186,144.19922 0,152.10039 0,161.90039 v 8.29883 c 0,9.7 7.8992186,17.70117 17.699219,17.70117 h 22.5 v -43.70117 z m 80.101562,0 v 43.70117 H 217.30078 c 9.7,0 17.69922,-8.00117 17.69922,-17.70117 v -8.29883 c 0,-9.8 -7.99922,-17.70117 -17.69922,-17.70117 z"/>
|
||||
<path d="M 301,60.564864 V 174.43514 h 53.31362 c 25.13729,0 38.31622,-12.58548 38.31622,-32.27441 0,-12.78766 -5.88,-22.32687 -17.63776,-27.60431 v -0.20217 c 8.91968,-5.48043 12.77339,-12.38249 12.77339,-23.140374 0,-16.238294 -11.14945,-30.648991 -34.66495,-30.648991 z m 110.68683,0 V 174.43514 h 13.37598 v -45.67022 l -1.41529,-1.41926 h 26.95811 c 15.00127,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 h 15.00139 l -16.82503,-35.52128 c -3.64896,-7.91617 -9.52848,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36593,-3.24765 22.70429,-14.41228 22.70429,-29.229734 0,-22.530633 -17.43208,-33.693671 -38.31224,-33.693671 z m 111.08726,0 V 174.43514 h 13.37992 V 60.564864 Z m 78.65821,0 -50.07469,113.870276 h 14.59701 l 12.16287,-27.40213 -0.60656,-1.41926 h 62.2336 l -0.60655,1.41926 12.16286,27.40213 h 14.59701 L 615.62098,60.564864 Z m 79.463,0 V 174.43514 h 13.37994 v -45.67022 l -1.41927,-1.41926 h 26.96209 c 15.00128,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 H 778 l -16.82503,-35.52128 c -3.64895,-7.91617 -9.52851,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36591,-3.24765 22.70427,-14.41228 22.70427,-29.229734 0,-22.530633 -17.43209,-33.693671 -38.31223,-33.693671 z M 312.96068,73.147961 h 38.72057 c 14.59584,0 22.29593,5.887175 22.29593,18.065895 0,10.148944 -6.07834,18.268094 -22.29593,18.268094 h -38.72057 l 1.41927,-1.41927 V 74.571187 Z m 110.68684,0 h 37.90786 c 13.78495,0 24.32519,5.684988 24.52791,20.908395 0,12.178724 -9.52687,20.702244 -25.94718,20.702244 h -36.48859 l 1.41529,-1.41927 V 74.571187 Z m 269.00626,0 h 37.90788 c 13.98769,0 24.53187,5.684988 24.53187,20.908395 0,12.178724 -9.52688,20.702244 -25.94718,20.702244 h -36.49257 l 1.41927,-1.41927 V 74.571187 Z m -83.92693,1.423226 h 0.20615 l 3.44509,11.366019 20.06794,45.670224 1.41924,1.41926 h -50.07071 l 1.41926,-1.41926 20.06793,-45.670224 z M 312.96068,122.06505 h 41.35294 c 16.82575,0 24.53189,7.71398 24.53189,20.09568 0,12.58468 -7.09797,19.69131 -24.53189,19.69131 h -41.35294 l 1.41927,-1.42322 v -36.94055 z"/>
|
||||
</svg>
|
||||
|
||||
<h2 id="download_title">Download Briar 1.2.20</h2>
|
||||
|
||||
<span id="download_intro">Someone nearby shared Briar with you.</span>
|
||||
|
||||
<a href="/app.apk" class="button">
|
||||
<svg aria-hidden="true"
|
||||
style="width:24px;height:24px;margin-right:6px;vertical-align:middle;"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
|
||||
</svg>
|
||||
<span id="download_button">Download Briar</span>
|
||||
</a>
|
||||
|
||||
<span id="download_outro">After the download is complete, open the downloaded file and install it.</span>
|
||||
</div>
|
||||
|
||||
<div id="bottom">
|
||||
<h3 id="troubleshooting_title">Troubleshooting</h3>
|
||||
<ol>
|
||||
<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>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,6 +46,7 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
/**
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
@@ -177,13 +178,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
|
||||
public void showNextFragment(BaseFragment f) {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) return;
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.addToBackStack(f.getUniqueTag())
|
||||
.commit();
|
||||
showFragment(getSupportFragmentManager(), f, f.getUniqueTag());
|
||||
}
|
||||
|
||||
protected boolean isFragmentAdded(String fragmentTag) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.briarproject.briar.android.contact.add.nearby.AddContactState.Contact
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementListening;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import static androidx.core.app.ActivityCompat.finishAfterTransition;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -95,15 +96,9 @@ public abstract class AbstractTabsFragment extends Fragment {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_help) {
|
||||
getParentFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out,
|
||||
R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, new HotspotHelpFragment(),
|
||||
HotspotHelpFragment.TAG)
|
||||
.addToBackStack(HotspotHelpFragment.TAG)
|
||||
.commit();
|
||||
Fragment f = new HotspotHelpFragment();
|
||||
String tag = HotspotHelpFragment.TAG;
|
||||
showFragment(getParentFragmentManager(), f, tag);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||
|
||||
/**
|
||||
* This class ensures that the conditions to open a hotspot are fulfilled.
|
||||
* <p>
|
||||
* Be sure to call {@link #onRequestPermissionResult(Boolean)} and
|
||||
* {@link #onRequestWifiEnabledResult()} when you get the
|
||||
* {@link ActivityResult}.
|
||||
* <p>
|
||||
* As soon as {@link #checkAndRequestConditions()} returns true,
|
||||
* all conditions are fulfilled.
|
||||
*/
|
||||
class ConditionManager {
|
||||
|
||||
private enum Permission {
|
||||
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
||||
}
|
||||
|
||||
private Permission locationPermission = Permission.UNKNOWN;
|
||||
private Permission wifiSetting = Permission.SHOW_RATIONALE;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to reset state when UI starts,
|
||||
* because state might have changed.
|
||||
*/
|
||||
void resetPermissions() {
|
||||
locationPermission = Permission.UNKNOWN;
|
||||
wifiSetting = Permission.SHOW_RATIONALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes a request for location permission.
|
||||
* If {@link #checkAndRequestConditions()} returns true, you can continue.
|
||||
*/
|
||||
void startConditionChecks() {
|
||||
locationRequest.launch(ACCESS_FINE_LOCATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if conditions are fulfilled and flow can continue.
|
||||
*/
|
||||
boolean checkAndRequestConditions() {
|
||||
if (areEssentialPermissionsGranted()) return true;
|
||||
|
||||
// If an essential permission has been permanently denied, ask the
|
||||
// user to change the setting
|
||||
if (locationPermission == Permission.PERMANENTLY_DENIED) {
|
||||
showDenialDialog(R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_denied_body,
|
||||
getGoToSettingsListener(ctx));
|
||||
return false;
|
||||
}
|
||||
if (wifiSetting == Permission.PERMANENTLY_DENIED) {
|
||||
showDenialDialog(R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_denied_body,
|
||||
(d, w) -> requestEnableWiFi());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Should we show the rationale for location permission or Wi-Fi?
|
||||
if (locationPermission == Permission.SHOW_RATIONALE) {
|
||||
showRationale(R.string.permission_location_title,
|
||||
R.string.permission_hotspot_location_request_body,
|
||||
this::requestPermissions);
|
||||
} else if (wifiSetting == Permission.SHOW_RATIONALE) {
|
||||
showRationale(R.string.wifi_settings_title,
|
||||
R.string.wifi_settings_request_enable_body,
|
||||
this::requestEnableWiFi);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void onRequestPermissionResult(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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,19 +3,25 @@ package org.briarproject.briar.android.hotspot;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_STOP_HOTSPOT;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -44,7 +50,21 @@ public class HotspotActivity extends BriarActivity {
|
||||
ab.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
// TODO observe viewmodel state and show error or HotspotFragment
|
||||
viewModel.getState().observe(this, hotspotState -> {
|
||||
if (hotspotState instanceof HotspotStarted) {
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
String tag = HotspotFragment.TAG;
|
||||
// check if fragment is already added
|
||||
// to not lose state on configuration changes
|
||||
if (fm.findFragmentByTag(tag) == null) {
|
||||
showFragment(fm, new HotspotFragment(), tag);
|
||||
}
|
||||
} else if (hotspotState instanceof HotspotError) {
|
||||
// TODO ErrorFragment
|
||||
String error = ((HotspotError) hotspotState).getError();
|
||||
Toast.makeText(this, error, LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
if (state == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
|
||||
@@ -5,11 +5,12 @@ import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.showFragment;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class HotspotFragment extends AbstractTabsFragment {
|
||||
@@ -21,14 +22,9 @@ public class HotspotFragment extends AbstractTabsFragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// no need to call into the ViewModel here
|
||||
connectedButton.setOnClickListener(v -> {
|
||||
getParentFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, new WebsiteFragment(),
|
||||
WebsiteFragment.TAG)
|
||||
.addToBackStack(WebsiteFragment.TAG)
|
||||
.commit();
|
||||
Fragment f = new WebsiteFragment();
|
||||
String tag = WebsiteFragment.TAG;
|
||||
showFragment(getParentFragmentManager(), f, tag);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -9,19 +12,27 @@ import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.transition.TransitionManager.beginDelayedTransition;
|
||||
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -34,13 +45,32 @@ public class HotspotIntroFragment extends Fragment {
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private HotspotViewModel viewModel;
|
||||
private ConditionManager conditionManager;
|
||||
|
||||
private Button startButton;
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressTextView;
|
||||
|
||||
private final ActivityResultLauncher<String> locationRequest =
|
||||
registerForActivityResult(new RequestPermission(), granted -> {
|
||||
conditionManager.onRequestPermissionResult(granted);
|
||||
startHotspot();
|
||||
});
|
||||
private final ActivityResultLauncher<Intent> wifiRequest =
|
||||
registerForActivityResult(new StartActivityForResult(), result -> {
|
||||
conditionManager.onRequestWifiEnabledResult();
|
||||
startHotspot();
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
getAndroidComponent(requireContext()).inject(this);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
FragmentActivity activity = requireActivity();
|
||||
getAndroidComponent(activity).inject(this);
|
||||
viewModel = new ViewModelProvider(activity, viewModelFactory)
|
||||
.get(HotspotViewModel.class);
|
||||
conditionManager =
|
||||
new ConditionManager(activity, locationRequest, wifiRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,31 +80,48 @@ public class HotspotIntroFragment extends Fragment {
|
||||
View v = inflater
|
||||
.inflate(R.layout.fragment_hotspot_intro, container, false);
|
||||
|
||||
Button startButton = v.findViewById(R.id.startButton);
|
||||
ProgressBar progressBar = v.findViewById(R.id.progressBar);
|
||||
TextView progressTextView = v.findViewById(R.id.progressTextView);
|
||||
startButton = v.findViewById(R.id.startButton);
|
||||
progressBar = v.findViewById(R.id.progressBar);
|
||||
progressTextView = v.findViewById(R.id.progressTextView);
|
||||
|
||||
startButton.setOnClickListener(button -> {
|
||||
beginDelayedTransition((ViewGroup) v);
|
||||
startButton.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
progressTextView.setVisibility(VISIBLE);
|
||||
// TODO remove below, tell viewModel to start hotspot instead
|
||||
v.postDelayed(() -> {
|
||||
viewModel.startHotspot();
|
||||
getParentFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out,
|
||||
R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, new HotspotFragment(),
|
||||
HotspotFragment.TAG)
|
||||
.addToBackStack(HotspotFragment.TAG)
|
||||
.commit();
|
||||
}, 1500);
|
||||
});
|
||||
startButton.setOnClickListener(
|
||||
button -> conditionManager.startConditionChecks());
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
conditionManager.resetPermissions();
|
||||
}
|
||||
|
||||
private void startHotspot() {
|
||||
if (conditionManager.checkAndRequestConditions()) {
|
||||
showInstallWarningIfNeeded();
|
||||
beginDelayedTransition((ViewGroup) requireView());
|
||||
startButton.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
progressTextView.setVisibility(VISIBLE);
|
||||
viewModel.startHotspot();
|
||||
}
|
||||
}
|
||||
|
||||
private void showInstallWarningIfNeeded() {
|
||||
Context ctx = requireContext();
|
||||
ApplicationInfo applicationInfo;
|
||||
try {
|
||||
applicationInfo = ctx.getPackageManager()
|
||||
.getApplicationInfo(ctx.getPackageName(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
// test only apps can not be installed
|
||||
if ((applicationInfo.flags & FLAG_TEST_ONLY) == FLAG_TEST_ONLY) {
|
||||
int color = getResources().getColor(R.color.briar_red_500);
|
||||
Snackbar.make(requireView(), R.string.hotspot_flag_test,
|
||||
LENGTH_LONG).setBackgroundTint(color).show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.net.wifi.p2p.WifiP2pConfig;
|
||||
import android.net.wifi.p2p.WifiP2pGroup;
|
||||
import android.net.wifi.p2p.WifiP2pManager;
|
||||
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
|
||||
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
|
||||
import android.os.Handler;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import static android.content.Context.WIFI_P2P_SERVICE;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
|
||||
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
|
||||
import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.BUSY;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.ERROR;
|
||||
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 java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
class HotspotManager implements ActionListener {
|
||||
|
||||
interface HotspotListener {
|
||||
void onStartingHotspot();
|
||||
|
||||
@IoExecutor
|
||||
void onHotspotStarted(NetworkConfig networkConfig);
|
||||
|
||||
void onHotspotStopped();
|
||||
|
||||
void onHotspotError(String error);
|
||||
}
|
||||
|
||||
private static final Logger LOG = getLogger(HotspotManager.class.getName());
|
||||
|
||||
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
|
||||
private static final int RETRY_DELAY_MILLIS = 1000;
|
||||
|
||||
private final Context ctx;
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
private final SecureRandom random;
|
||||
private final HotspotListener listener;
|
||||
private final WifiManager wifiManager;
|
||||
private final WifiP2pManager wifiP2pManager;
|
||||
private final Handler handler;
|
||||
private final String lockTag;
|
||||
|
||||
@Nullable
|
||||
// on API < 29 this is null because we cannot request a custom network name
|
||||
private String networkName = null;
|
||||
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private WifiP2pManager.Channel channel;
|
||||
|
||||
HotspotManager(Context ctx, @IoExecutor Executor ioExecutor,
|
||||
SecureRandom random, HotspotListener listener) {
|
||||
this.ctx = ctx;
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.random = random;
|
||||
this.listener = listener;
|
||||
wifiManager = (WifiManager) ctx.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
wifiP2pManager =
|
||||
(WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
|
||||
handler = new Handler(ctx.getMainLooper());
|
||||
lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void startWifiP2pHotspot() {
|
||||
if (wifiP2pManager == null) {
|
||||
listener.onHotspotError(
|
||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||
return;
|
||||
}
|
||||
listener.onStartingHotspot();
|
||||
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
|
||||
if (channel == null) {
|
||||
listener.onHotspotError(
|
||||
ctx.getString(R.string.hotspot_error_no_wifi_direct));
|
||||
return;
|
||||
}
|
||||
acquireLock();
|
||||
try {
|
||||
if (SDK_INT >= 29) {
|
||||
networkName = getNetworkName();
|
||||
String passphrase = getPassphrase();
|
||||
WifiP2pConfig config = new WifiP2pConfig.Builder()
|
||||
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
|
||||
.setNetworkName(networkName)
|
||||
.setPassphrase(passphrase)
|
||||
.build();
|
||||
wifiP2pManager.createGroup(channel, config, this);
|
||||
} else {
|
||||
wifiP2pManager.createGroup(channel, this);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// this should never happen, because we request permissions before
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
|
||||
public void onSuccess() {
|
||||
requestGroupInfo(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
|
||||
public void onFailure(int reason) {
|
||||
if (reason == BUSY) {
|
||||
// Hotspot already running
|
||||
requestGroupInfo(1);
|
||||
} else if (reason == P2P_UNSUPPORTED) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed,
|
||||
"p2p unsupported"));
|
||||
} else if (reason == ERROR) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed, "p2p error"));
|
||||
} else if (reason == NO_SERVICE_REQUESTS) {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed,
|
||||
"no service requests"));
|
||||
} else {
|
||||
// all cases covered, in doubt set to error
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_failed_unknown,
|
||||
reason));
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
private String getNetworkName() {
|
||||
return "DIRECT-" + getRandomString(2) + "-" +
|
||||
getRandomString(10);
|
||||
}
|
||||
|
||||
private String getPassphrase() {
|
||||
return getRandomString(8);
|
||||
}
|
||||
|
||||
void stopWifiP2pHotspot() {
|
||||
if (channel == null) return;
|
||||
wifiP2pManager.removeGroup(channel, new ActionListener() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
releaseHotspot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int reason) {
|
||||
// not propagating back error
|
||||
releaseHotspot();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void acquireLock() {
|
||||
// WIFI_MODE_FULL has no effect on API >= 29
|
||||
int lockType =
|
||||
SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
|
||||
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
|
||||
wifiLock.acquire();
|
||||
}
|
||||
|
||||
private void releaseHotspot() {
|
||||
listener.onHotspotStopped();
|
||||
closeChannelAndReleaseLock();
|
||||
}
|
||||
|
||||
private void releaseHotspotWithError(String error) {
|
||||
listener.onHotspotError(error);
|
||||
closeChannelAndReleaseLock();
|
||||
}
|
||||
|
||||
private void closeChannelAndReleaseLock() {
|
||||
if (SDK_INT >= 27) channel.close();
|
||||
channel = null;
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private void requestGroupInfo(int attempt) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("requestGroupInfo attempt: " + attempt);
|
||||
}
|
||||
GroupInfoListener groupListener = group -> {
|
||||
boolean valid = isGroupValid(group);
|
||||
// If the group is valid, set the hotspot to started. If we don't
|
||||
// have any attempts left, we try what we got
|
||||
if (valid || attempt >= MAX_GROUP_INFO_ATTEMPTS) {
|
||||
onHotspotStarted(group);
|
||||
} else {
|
||||
retryRequestingGroupInfo(attempt + 1);
|
||||
}
|
||||
};
|
||||
try {
|
||||
wifiP2pManager.requestGroupInfo(channel, groupListener);
|
||||
} catch (SecurityException e) {
|
||||
// this should never happen, because we request permissions before
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onHotspotStarted(WifiP2pGroup group) {
|
||||
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
|
||||
ioExecutor.execute(() -> {
|
||||
String content = createWifiLoginString(group.getNetworkName(),
|
||||
group.getPassphrase());
|
||||
Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
|
||||
NetworkConfig config = new NetworkConfig(group.getNetworkName(),
|
||||
group.getPassphrase(), qrCode);
|
||||
listener.onHotspotStarted(config);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isGroupValid(@Nullable WifiP2pGroup group) {
|
||||
if (group == null) {
|
||||
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());
|
||||
}
|
||||
return false;
|
||||
} else if (networkName != null &&
|
||||
!networkName.equals(group.getNetworkName())) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("expected networkName: " + networkName);
|
||||
LOG.info("received networkName: " + group.getNetworkName());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void retryRequestingGroupInfo(int attempt) {
|
||||
LOG.info("retrying");
|
||||
// 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),
|
||||
RETRY_DELAY_MILLIS);
|
||||
} else {
|
||||
releaseHotspotWithError(ctx.getString(
|
||||
R.string.hotspot_error_start_callback_no_group_info));
|
||||
}
|
||||
}
|
||||
|
||||
private static String createWifiLoginString(String ssid, String password) {
|
||||
// https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
|
||||
// do not remove the dangling ';', it can cause problems to omit it
|
||||
return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
|
||||
}
|
||||
|
||||
private static final String digits = "123456789"; // avoid 0
|
||||
private static final String letters = "abcdefghijkmnopqrstuvwxyz"; // no l
|
||||
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no I, O
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return new String(c);
|
||||
}
|
||||
|
||||
private char random(String universe) {
|
||||
return universe.charAt(random.nextInt(universe.length()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
abstract class HotspotState {
|
||||
|
||||
static class StartingHotspot extends HotspotState {
|
||||
}
|
||||
|
||||
static class NetworkConfig {
|
||||
final String ssid, password;
|
||||
@Nullable
|
||||
final Bitmap qrCode;
|
||||
|
||||
NetworkConfig(String ssid, String password, @Nullable Bitmap qrCode) {
|
||||
this.ssid = ssid;
|
||||
this.password = password;
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
}
|
||||
|
||||
static class WebsiteConfig {
|
||||
final String url;
|
||||
@Nullable
|
||||
final Bitmap qrCode;
|
||||
|
||||
WebsiteConfig(String url, @Nullable Bitmap qrCode) {
|
||||
this.url = url;
|
||||
this.qrCode = qrCode;
|
||||
}
|
||||
}
|
||||
|
||||
static class HotspotStarted extends HotspotState {
|
||||
private final NetworkConfig networkConfig;
|
||||
private final WebsiteConfig websiteConfig;
|
||||
|
||||
HotspotStarted(NetworkConfig networkConfig,
|
||||
WebsiteConfig websiteConfig) {
|
||||
this.networkConfig = networkConfig;
|
||||
this.websiteConfig = websiteConfig;
|
||||
}
|
||||
|
||||
NetworkConfig getNetworkConfig() {
|
||||
return networkConfig;
|
||||
}
|
||||
|
||||
WebsiteConfig getWebsiteConfig() {
|
||||
return websiteConfig;
|
||||
}
|
||||
}
|
||||
|
||||
static class HotspotError extends HotspotState {
|
||||
private final String error;
|
||||
|
||||
HotspotError(String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,37 +4,82 @@ import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.hotspot.HotspotManager.HotspotListener;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.StartingHotspot;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
|
||||
import org.briarproject.briar.android.hotspot.WebServerManager.WebServerListener;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@NotNullByDefault
|
||||
class HotspotViewModel extends DbViewModel {
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
|
||||
@NotNullByDefault
|
||||
class HotspotViewModel extends DbViewModel
|
||||
implements HotspotListener, WebServerListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(HotspotViewModel.class.getName());
|
||||
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
private final AndroidNotificationManager notificationManager;
|
||||
private final HotspotManager hotspotManager;
|
||||
private final WebServerManager webServerManager;
|
||||
|
||||
private final MutableLiveData<HotspotState> state =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Nullable
|
||||
// Field to temporarily store the network config received via onHotspotStarted()
|
||||
// in order to post it along with a HotspotStarted status
|
||||
private volatile NetworkConfig networkConfig;
|
||||
|
||||
@Inject
|
||||
HotspotViewModel(Application application,
|
||||
HotspotViewModel(Application app,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
@IoExecutor Executor ioExecutor,
|
||||
SecureRandom secureRandom,
|
||||
AndroidNotificationManager notificationManager) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.ioExecutor = ioExecutor;
|
||||
this.notificationManager = notificationManager;
|
||||
hotspotManager =
|
||||
new HotspotManager(app, ioExecutor, secureRandom, this);
|
||||
webServerManager = new WebServerManager(app, this);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void startHotspot() {
|
||||
hotspotManager.startWifiP2pHotspot();
|
||||
notificationManager.showHotspotNotification();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void stopHotspot() {
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
hotspotManager.stopWifiP2pHotspot();
|
||||
notificationManager.clearHotspotNotification();
|
||||
}
|
||||
|
||||
@@ -44,6 +89,49 @@ class HotspotViewModel extends DbViewModel {
|
||||
stopHotspot();
|
||||
}
|
||||
|
||||
// TODO copy actual code from Offline Hotspot app
|
||||
@Override
|
||||
public void onStartingHotspot() {
|
||||
state.setValue(new StartingHotspot());
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onHotspotStarted(NetworkConfig networkConfig) {
|
||||
this.networkConfig = networkConfig;
|
||||
LOG.info("starting webserver");
|
||||
webServerManager.startWebServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotspotStopped() {
|
||||
LOG.info("stopping webserver");
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHotspotError(String error) {
|
||||
state.setValue(new HotspotError(error));
|
||||
ioExecutor.execute(webServerManager::stopWebServer);
|
||||
notificationManager.clearHotspotNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onWebServerStarted(WebsiteConfig websiteConfig) {
|
||||
state.postValue(new HotspotStarted(networkConfig, websiteConfig));
|
||||
networkConfig = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@IoExecutor
|
||||
public void onWebServerError() {
|
||||
state.postValue(new HotspotError(getApplication()
|
||||
.getString(R.string.hotspot_error_web_server_start)));
|
||||
hotspotManager.stopWifiP2pHotspot();
|
||||
}
|
||||
|
||||
LiveData<HotspotState> getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ import org.briarproject.briar.R;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
|
||||
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
|
||||
import static org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -66,21 +68,28 @@ public class ManualHotspotFragment extends Fragment {
|
||||
TextView passwordView = v.findViewById(R.id.passwordView);
|
||||
TextView altView = v.findViewById(R.id.altView);
|
||||
|
||||
Consumer<HotspotStarted> consumer;
|
||||
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
|
||||
manualIntroView.setText(R.string.hotspot_manual_wifi);
|
||||
ssidLabelView.setText(R.string.hotspot_manual_wifi_ssid);
|
||||
// TODO observe state in ViewModel and get info from there instead
|
||||
ssidView.setText("DIRECT-42-dfzsgf34ef");
|
||||
passwordView.setText("sdf78shfd8");
|
||||
consumer = state -> {
|
||||
ssidView.setText(state.getNetworkConfig().ssid);
|
||||
passwordView.setText(state.getNetworkConfig().password);
|
||||
};
|
||||
altView.setText(R.string.hotspot_manual_wifi_alt);
|
||||
} else {
|
||||
manualIntroView.setText(R.string.hotspot_manual_site);
|
||||
ssidLabelView.setText(R.string.hotspot_manual_site_address);
|
||||
// TODO observe state in ViewModel and get info from there instead
|
||||
ssidView.setText("http://192.168.49.1:9999");
|
||||
consumer = state -> ssidView.setText(state.getWebsiteConfig().url);
|
||||
altView.setText(R.string.hotspot_manual_site_alt);
|
||||
v.findViewById(R.id.passwordLabelView).setVisibility(GONE);
|
||||
passwordView.setVisibility(GONE);
|
||||
}
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
// we only expect to be in this state here
|
||||
if (state instanceof HotspotStarted) {
|
||||
consumer.accept((HotspotStarted) state);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import android.widget.TextView;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
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;
|
||||
|
||||
@@ -58,13 +60,21 @@ 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);
|
||||
// TODO observe state in ViewModel and get QR code from there
|
||||
consumer = state ->
|
||||
qrCodeView.setImageBitmap(state.getNetworkConfig().qrCode);
|
||||
} else {
|
||||
qrIntroView.setText(R.string.hotspot_qr_site);
|
||||
// TODO observe state in ViewModel and get QR code from there
|
||||
consumer = state ->
|
||||
qrCodeView.setImageBitmap(state.getWebsiteConfig().qrCode);
|
||||
}
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
if (state instanceof HotspotStarted) {
|
||||
consumer.accept((HotspotStarted) state);
|
||||
}
|
||||
});
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
import static android.util.Xml.Encoding.UTF_8;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.briar.BuildConfig.VERSION_NAME;
|
||||
|
||||
public class WebServer extends NanoHTTPD {
|
||||
|
||||
final static int PORT = 9999;
|
||||
|
||||
private static final Logger LOG = getLogger(WebServer.class.getName());
|
||||
private static final String FILE_HTML = "hotspot.html";
|
||||
private static final Pattern REGEX_AGENT =
|
||||
Pattern.compile("Android ([0-9]+)");
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
WebServer(Context ctx) {
|
||||
super(PORT);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws IOException {
|
||||
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
if (session.getUri().endsWith("favicon.ico")) {
|
||||
return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
|
||||
NOT_FOUND.getDescription());
|
||||
}
|
||||
if (session.getUri().endsWith(".apk")) {
|
||||
return serveApk();
|
||||
}
|
||||
Response res;
|
||||
try {
|
||||
String html = getHtml(session.getHeaders().get("user-agent"));
|
||||
res = newFixedLengthResponse(OK, MIME_HTML, html);
|
||||
} catch (Exception e) {
|
||||
logException(LOG, WARNING, e);
|
||||
res = newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT,
|
||||
INTERNAL_ERROR.getDescription());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private String getHtml(@Nullable String userAgent) throws Exception {
|
||||
Document doc;
|
||||
try (InputStream is = ctx.getAssets().open(FILE_HTML)) {
|
||||
doc = Jsoup.parse(is, UTF_8.name(), "");
|
||||
}
|
||||
String app = ctx.getString(R.string.app_name);
|
||||
String appV = app + " " + VERSION_NAME;
|
||||
doc.select("#download_title").first()
|
||||
.text(ctx.getString(R.string.website_download_title, appV));
|
||||
doc.select("#download_intro").first()
|
||||
.text(ctx.getString(R.string.website_download_intro, app));
|
||||
doc.select("#download_button").first()
|
||||
.text(ctx.getString(R.string.website_download_title, app));
|
||||
doc.select("#download_outro").first()
|
||||
.text(ctx.getString(R.string.website_download_outro));
|
||||
doc.select("#troubleshooting_title").first()
|
||||
.text(ctx.getString(R.string.website_troubleshooting_title));
|
||||
doc.select("#troubleshooting_1").first()
|
||||
.text(ctx.getString(R.string.website_troubleshooting_1));
|
||||
doc.select("#troubleshooting_2").first()
|
||||
.text(getUnknownSourcesString(userAgent));
|
||||
return doc.outerHtml();
|
||||
}
|
||||
|
||||
private String getUnknownSourcesString(String userAgent) {
|
||||
boolean is8OrHigher = false;
|
||||
if (userAgent != null) {
|
||||
Matcher matcher = REGEX_AGENT.matcher(userAgent);
|
||||
if (matcher.find()) {
|
||||
int androidMajorVersion =
|
||||
Integer.parseInt(requireNonNull(matcher.group(1)));
|
||||
is8OrHigher = androidMajorVersion >= 8;
|
||||
}
|
||||
}
|
||||
return is8OrHigher ?
|
||||
ctx.getString(R.string.website_troubleshooting_2_new) :
|
||||
ctx.getString(R.string.website_troubleshooting_2_old);
|
||||
}
|
||||
|
||||
private Response serveApk() {
|
||||
String mime = "application/vnd.android.package-archive";
|
||||
|
||||
File file = new File(ctx.getPackageCodePath());
|
||||
long fileLen = file.length();
|
||||
|
||||
Response res;
|
||||
try {
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
res = newFixedLengthResponse(OK, mime, fis, fileLen);
|
||||
res.addHeader("Content-Length", "" + fileLen);
|
||||
} catch (FileNotFoundException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
res = newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
|
||||
"Error 404, file not found.");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.briarproject.briar.android.hotspot;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
|
||||
import org.briarproject.briar.android.util.QrCodeUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.list;
|
||||
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.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.briar.android.hotspot.WebServer.PORT;
|
||||
|
||||
class WebServerManager {
|
||||
|
||||
interface WebServerListener {
|
||||
@IoExecutor
|
||||
void onWebServerStarted(WebsiteConfig websiteConfig);
|
||||
|
||||
@IoExecutor
|
||||
void onWebServerError();
|
||||
}
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(WebServerManager.class.getName());
|
||||
|
||||
private final WebServer webServer;
|
||||
private final WebServerListener listener;
|
||||
private final DisplayMetrics dm;
|
||||
|
||||
WebServerManager(Context ctx, WebServerListener listener) {
|
||||
this.listener = listener;
|
||||
webServer = new WebServer(ctx);
|
||||
dm = ctx.getResources().getDisplayMetrics();
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
void startWebServer() {
|
||||
try {
|
||||
webServer.start();
|
||||
onWebServerStarted();
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
listener.onWebServerError();
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void onWebServerStarted() {
|
||||
String url = "http://192.168.49.1:" + PORT;
|
||||
InetAddress address = getAccessPointAddress();
|
||||
if (address == null) {
|
||||
LOG.info(
|
||||
"Could not find access point address, assuming 192.168.49.1");
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Access point address " + address.getHostAddress());
|
||||
}
|
||||
url = "http://" + address.getHostAddress() + ":" + PORT;
|
||||
}
|
||||
Bitmap qrCode = QrCodeUtils.createQrCode(dm, url);
|
||||
listener.onWebServerStarted(new WebsiteConfig(url, qrCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* It is safe to call this more than once and it won't throw.
|
||||
*/
|
||||
@IoExecutor
|
||||
void stopWebServer() {
|
||||
webServer.stop();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static InetAddress getAccessPointAddress() {
|
||||
for (NetworkInterface i : getNetworkInterfaces()) {
|
||||
if (i.getName().startsWith("p2p")) {
|
||||
for (InterfaceAddress a : i.getInterfaceAddresses()) {
|
||||
// we consider only IPv4 addresses
|
||||
if (a.getAddress().getAddress().length == 4)
|
||||
return a.getAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<NetworkInterface> getNetworkInterfaces() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> ifaces =
|
||||
NetworkInterface.getNetworkInterfaces();
|
||||
return ifaces == null ? emptyList() : list(ifaces);
|
||||
} catch (SocketException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.contact.add.nearby;
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -22,12 +22,12 @@ import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class QrCodeUtils {
|
||||
public class QrCodeUtils {
|
||||
|
||||
private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
|
||||
|
||||
@Nullable
|
||||
static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
public static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
|
||||
try {
|
||||
// Generate QR code
|
||||
@@ -58,7 +58,9 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -139,6 +141,17 @@ public class UiUtils {
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
public static void showFragment(FragmentManager fm, Fragment f,
|
||||
@Nullable String tag) {
|
||||
fm.beginTransaction()
|
||||
.setCustomAnimations(R.anim.step_next_in,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, f, tag)
|
||||
.addToBackStack(tag)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static String getContactDisplayName(Author author,
|
||||
@Nullable String alias) {
|
||||
String name = author.getName();
|
||||
|
||||
@@ -17,35 +17,30 @@
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/hotspot_qr_wifi"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/cardView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="64dp"
|
||||
android:layout_marginRight="64dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1,1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/qrIntroView"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside">
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qrCodeView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:srcCompat="@drawable/ic_qr_code"
|
||||
tools:ignore="ContentDescription" />
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
@@ -701,6 +701,20 @@
|
||||
<string name="hotspot_notification_channel_title">Wi-Fi hotspot</string>
|
||||
<string name="hotspot_notification_title">Sharing Briar offline</string>
|
||||
<string name="hotspot_button_connected">Confirm connection</string>
|
||||
|
||||
<string name="permission_hotspot_location_request_body">To create a Wi-Fi hotspot, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
|
||||
<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="hotspot_error_no_wifi_direct">Device does not support Wi-Fi Direct</string>
|
||||
<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_flag_test">Warning: This is a debug app that can NOT be installed on another device</string>
|
||||
|
||||
<string name="hotspot_tab_manual">Manual</string>
|
||||
<string name="hotspot_manual_wifi">To download the app on another phone, please connect to this Wi-Fi network:</string>
|
||||
<string name="hotspot_manual_wifi_ssid">Network name (SSID)</string>
|
||||
@@ -710,6 +724,16 @@
|
||||
<string name="hotspot_manual_site_alt">Instead of typing the address manually, you can also scan a QR code.</string>
|
||||
<string name="hotspot_qr_wifi">To download the app on another phone, please scan this QR code to connect to this Wi-Fi network:</string>
|
||||
<string name="hotspot_qr_site">After you are connected to the Wi-Fi, scan this QR code to download the app.</string>
|
||||
|
||||
<!-- e.g. Download Briar 1.2.20 -->
|
||||
<string name="website_download_title">Download %s</string>
|
||||
<string name="website_download_intro">Someone nearby shared %s with you.</string>
|
||||
<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.</string>
|
||||
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser 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>
|
||||
|
||||
@@ -216,6 +216,7 @@ dependencyVerification {
|
||||
'org.jmock:jmock:2.8.2:jmock-2.8.2.jar:6c73cb4a2e6dbfb61fd99c9a768539c170ab6568e57846bd60dbf19596b65b16',
|
||||
'org.jvnet.staxex:stax-ex:1.8:stax-ex-1.8.jar:95b05d9590af4154c6513b9c5dc1fb2e55b539972ba0a9ef28e9a0c01d83ad77',
|
||||
'org.mockito:mockito-core:3.1.0:mockito-core-3.1.0.jar:89b09e518e04f5c35f5ccf7abe45e72f594070a53d95cc2579001bd392c5afa6',
|
||||
'org.nanohttpd:nanohttpd:2.3.1:nanohttpd-2.3.1.jar:de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58',
|
||||
'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d',
|
||||
'org.ow2.asm:asm-analysis:7.0:asm-analysis-7.0.jar:e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474',
|
||||
'org.ow2.asm:asm-commons:7.0:asm-commons-7.0.jar:fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d',
|
||||
|
||||
Reference in New Issue
Block a user