Skip to content
Snippets Groups Projects
Commit 832b08c1 authored by Sebastian Kürten's avatar Sebastian Kürten
Browse files

Move hotspot managing code into separate class

parent c5f12d4f
No related branches found
No related tags found
1 merge request!6ViewModel cleanup
......@@ -18,10 +18,12 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.hotspot.HotspotManager.UNKNOWN_FREQUENCY;
import static org.briarproject.hotspot.QrCodeUtils.createQrCode;
import static org.briarproject.hotspot.QrCodeUtils.createWifiLoginString;
......@@ -69,7 +71,7 @@ public class HotspotFragment extends Fragment {
serverButton = v.findViewById(R.id.serverButton);
serverButton.setOnClickListener(this::onServerButtonClick);
viewModel.getWifiConfiguration()
viewModel.getHotSpotManager().getWifiConfiguration()
.observe(getViewLifecycleOwner(), config -> {
if (config == null) {
qrCode.setVisibility(GONE);
......@@ -99,8 +101,66 @@ public class HotspotFragment extends Fragment {
}
});
viewModel.getStatus().observe(getViewLifecycleOwner(),
status -> statusView.setText(status));
viewModel.getIs5GhzSupported().observe(getViewLifecycleOwner(),
b -> statusView
.setText(getString(R.string.wifi_5ghz_supported)));
viewModel.getHotSpotManager().getStatus()
.observe(getViewLifecycleOwner(), state -> {
switch (state) {
case NO_WIFI_DIRECT:
statusView.setText(
getString(R.string.no_wifi_direct));
break;
case P2P_ERROR:
statusView.setText(
getString(R.string.callback_failed,
"p2p error"));
break;
case P2P_P2P_UNSUPPORTED:
statusView.setText(
getString(R.string.callback_failed,
"p2p unsupported"));
break;
case P2P_NO_SERVICE_REQUESTS:
statusView.setText(
getString(R.string.callback_failed,
"no service requests"));
break;
case STARTING_HOTSPOT:
statusView.setText(
getString(R.string.starting_hotspot));
break;
case CALLBACK_STARTED:
LiveData<Double> frequency =
viewModel.getHotSpotManager()
.getFrequency();
if (frequency.getValue() != null) {
double freq = frequency.getValue();
if (freq == UNKNOWN_FREQUENCY)
statusView.setText(getString(
R.string.callback_started));
else statusView.setText(getString(
R.string.callback_started_freq, freq));
}
break;
case WAITING_TO_START_HOTSPOT:
statusView.setText(
getString(R.string.callback_waiting));
break;
case HOTSPOT_STOPPED:
statusView.setText(
getString(R.string.hotspot_stopped));
break;
case PERMISSION_DENIED:
statusView.setText(getString(
R.string.callback_permission_denied));
break;
case NO_GROUP_INFO:
statusView.setText(
getString(R.string.callback_no_group_info));
break;
}
});
viewModel.getWebServerState()
.observe(getViewLifecycleOwner(), state -> {
......@@ -123,7 +183,7 @@ public class HotspotFragment extends Fragment {
public void onButtonClick(View view) {
if (hotspotStarted) {
button.setEnabled(false);
viewModel.stopWifiP2pHotspot();
viewModel.getHotSpotManager().stopWifiP2pHotspot();
} else {
conditionManager.startConditionChecks();
}
......
package org.briarproject.hotspot;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
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 org.briarproject.hotspot.HotspotManager.HotspotState.CALLBACK_STARTED;
import static org.briarproject.hotspot.HotspotManager.HotspotState.HOTSPOT_STOPPED;
import static org.briarproject.hotspot.HotspotManager.HotspotState.NO_GROUP_INFO;
import static org.briarproject.hotspot.HotspotManager.HotspotState.NO_WIFI_DIRECT;
import static org.briarproject.hotspot.HotspotManager.HotspotState.P2P_ERROR;
import static org.briarproject.hotspot.HotspotManager.HotspotState.P2P_NO_SERVICE_REQUESTS;
import static org.briarproject.hotspot.HotspotManager.HotspotState.P2P_P2P_UNSUPPORTED;
import static org.briarproject.hotspot.HotspotManager.HotspotState.PERMISSION_DENIED;
import static org.briarproject.hotspot.HotspotManager.HotspotState.STARTING_HOTSPOT;
import static org.briarproject.hotspot.HotspotManager.HotspotState.WAITING_TO_START_HOTSPOT;
import static org.briarproject.hotspot.StringUtils.getRandomString;
public class HotspotManager {
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
enum HotspotState {
NO_WIFI_DIRECT,
P2P_ERROR,
P2P_P2P_UNSUPPORTED,
P2P_NO_SERVICE_REQUESTS,
STARTING_HOTSPOT,
CALLBACK_STARTED,
WAITING_TO_START_HOTSPOT,
HOTSPOT_STOPPED,
PERMISSION_DENIED,
NO_GROUP_INFO,
}
static final double UNKNOWN_FREQUENCY = Double.NEGATIVE_INFINITY;
private final Context context;
private final WifiManager wifiManager;
private final WifiP2pManager wifiP2pManager;
private final HotspotListener listener;
private final Handler handler;
private final String lockTag;
private final MutableLiveData<NetworkConfig> config =
new MutableLiveData<>();
private final MutableLiveData<HotspotState> status =
new MutableLiveData<>();
private final MutableLiveData<Double> frequency = new MutableLiveData<>();
private WifiManager.WifiLock wifiLock;
private WifiP2pManager.Channel channel;
public HotspotManager(Context context, HotspotListener listener) {
this.context = context;
this.listener = listener;
wifiManager = (WifiManager) context.getApplicationContext()
.getSystemService(WIFI_SERVICE);
wifiP2pManager =
(WifiP2pManager) context.getSystemService(WIFI_P2P_SERVICE);
handler = new Handler(context.getMainLooper());
lockTag = context.getString(R.string.app_name);
}
LiveData<HotspotState> getStatus() {
return status;
}
LiveData<Double> getFrequency() {
return frequency;
}
LiveData<NetworkConfig> getWifiConfiguration() {
return config;
}
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
status.setValue(NO_WIFI_DIRECT);
return;
}
status.setValue(STARTING_HOTSPOT);
channel = wifiP2pManager
.initialize(context, context.getMainLooper(), null);
if (channel == null) {
status.setValue(NO_WIFI_DIRECT);
return;
}
acquireLock();
String networkName =
SDK_INT >= 29 ? "DIRECT-" + getRandomString(2) + "-" +
getRandomString(10) : null;
WifiP2pManager.ActionListener listener =
new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
status.setValue(WAITING_TO_START_HOTSPOT);
requestGroupInfo(1, networkName);
}
@Override
public void onFailure(int reason) {
if (reason == BUSY) requestGroupInfo(1,
networkName); // Hotspot already running
else if (reason == P2P_UNSUPPORTED)
releaseWifiP2pHotspot(P2P_P2P_UNSUPPORTED);
else if (reason == ERROR)
releaseWifiP2pHotspot(P2P_ERROR);
else if (reason == NO_SERVICE_REQUESTS)
releaseWifiP2pHotspot(P2P_NO_SERVICE_REQUESTS);
else releaseWifiP2pHotspot(P2P_ERROR);
// all cases covered, in doubt set to error
}
};
try {
if (SDK_INT >= 29) {
String passphrase = getRandomString(8);
Log.e("TEST", "networkName: " + networkName);
Log.e("TEST", "passphrase: " + passphrase);
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(networkName)
.setPassphrase(passphrase)
.build();
wifiP2pManager.createGroup(channel, config, listener);
} else {
wifiP2pManager.createGroup(channel, listener);
}
} catch (SecurityException e) {
releaseWifiP2pHotspot(PERMISSION_DENIED);
}
}
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager
.removeGroup(channel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
releaseWifiP2pHotspot(HOTSPOT_STOPPED);
}
@Override
public void onFailure(int reason) {
releaseWifiP2pHotspot(HOTSPOT_STOPPED);
}
});
}
@SuppressLint("WakelockTimeout")
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 releaseLock() {
wifiLock.release();
}
private void releaseWifiP2pHotspot(HotspotState status) {
listener.disconnected();
if (SDK_INT >= 27) channel.close();
channel = null;
releaseLock();
config.setValue(null);
this.status.setValue(status);
}
private void requestGroupInfo(int attempt, @Nullable String networkName) {
Log.e("TEST", "requestGroupInfo attempt: " + attempt);
WifiP2pManager.GroupInfoListener listener = group -> {
boolean retry = false;
if (group == null) {
Log.e("TEST", "group is null");
retry = true;
} else if (!group.getNetworkName().startsWith("DIRECT-") ||
(networkName != null &&
!networkName.equals(group.getNetworkName()))) {
// we only retry if we have attempts left, otherwise we try what we got
if (attempt < MAX_GROUP_INFO_ATTEMPTS) retry = true;
Log.e("TEST", "expected networkName: " + networkName);
Log.e("TEST",
"received networkName: " + group.getNetworkName());
Log.e("TEST", "received passphrase: " + group.getPassphrase());
}
if (retry) {
Log.e("TEST", "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, networkName),
1000);
} else {
releaseWifiP2pHotspot(NO_GROUP_INFO);
}
} else {
config.setValue(new NetworkConfig(group.getNetworkName(),
group.getPassphrase(),
true));
if (SDK_INT >= 29) {
frequency.setValue(((double) group.getFrequency()) / 1000);
status.setValue(CALLBACK_STARTED);
} else {
frequency.setValue(UNKNOWN_FREQUENCY);
status.setValue(CALLBACK_STARTED);
}
this.listener.connected();
}
};
try {
wifiP2pManager.requestGroupInfo(channel, listener);
} catch (SecurityException e) {
releaseWifiP2pHotspot(PERMISSION_DENIED);
}
}
interface HotspotListener {
void connected();
void disconnected();
}
static class NetworkConfig {
final String ssid, password;
final boolean hidden;
NetworkConfig(String ssid, String password, boolean hidden) {
this.ssid = ssid;
this.password = password;
this.hidden = hidden;
}
}
}
package org.briarproject.hotspot;
import android.annotation.SuppressLint;
import android.app.Application;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
import android.os.Handler;
import android.util.Log;
import java.io.IOException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
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.os.Build.VERSION.SDK_INT;
import static org.briarproject.hotspot.StringUtils.getRandomString;
import static org.briarproject.hotspot.HotspotManager.HotspotListener;
public class MainViewModel extends AndroidViewModel {
public class MainViewModel extends AndroidViewModel implements HotspotListener {
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
private final MutableLiveData<NetworkConfig> config =
new MutableLiveData<>();
private final MutableLiveData<String> status = new MutableLiveData<>();
private final MutableLiveData<Boolean> is5GhzSupported =
new MutableLiveData<>(false);
private final MutableLiveData<WebServerState> webServerState =
new MutableLiveData<>(WebServerState.STOPPED);
private final Application app;
private final String lockTag;
private final WifiManager wifiManager;
private final WifiP2pManager wifiP2pManager;
private final Handler handler;
private final HotspotManager hotSpotManager;
private final WebServer webServer;
private WifiLock wifiLock;
private Channel channel;
public MainViewModel(@NonNull Application application) {
super(application);
app = application;
lockTag = app.getString(R.string.app_name);
wifiManager = (WifiManager) app.getSystemService(WIFI_SERVICE);
wifiP2pManager =
(WifiP2pManager) app.getSystemService(WIFI_P2P_SERVICE);
handler = new Handler(app.getMainLooper());
public MainViewModel(@NonNull Application app) {
super(app);
hotSpotManager = new HotspotManager(app, this);
webServer = new WebServer(app);
if (SDK_INT >= 21 && wifiManager.is5GHzBandSupported()) {
status.setValue(app.getString(R.string.wifi_5ghz_supported));
if (SDK_INT >= 21) {
WifiManager wifiManager =
(WifiManager) app.getSystemService(WIFI_SERVICE);
if (wifiManager.is5GHzBandSupported()) {
is5GhzSupported.setValue(true);
}
}
}
LiveData<NetworkConfig> getWifiConfiguration() {
return config;
}
LiveData<String> getStatus() {
return status;
LiveData<Boolean> getIs5GhzSupported() {
return is5GhzSupported;
}
LiveData<WebServerState> getWebServerState() {
return webServerState;
}
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
status.setValue(app.getString(R.string.no_wifi_direct));
return;
}
status.setValue(app.getString(R.string.starting_hotspot));
channel = wifiP2pManager.initialize(app, app.getMainLooper(), null);
if (channel == null) {
status.setValue(app.getString(R.string.no_wifi_direct));
return;
}
acquireLock();
String networkName = SDK_INT >= 29 ? "DIRECT-"
+ getRandomString(2) + "-" + getRandomString(10) : null;
ActionListener listener = new ActionListener() {
@Override
public void onSuccess() {
status.setValue(app.getString(R.string.callback_waiting));
requestGroupInfo(1, networkName);
}
@Override
public void onFailure(int reason) {
if (reason == 2)
requestGroupInfo(1, networkName); // Hotspot already running
else releaseWifiP2pHotspot(
app.getString(R.string.callback_failed, reason));
}
};
try {
if (SDK_INT >= 29) {
String passphrase = getRandomString(8);
Log.e("TEST", "networkName: " + networkName);
Log.e("TEST", "passphrase: " + passphrase);
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(networkName)
.setPassphrase(passphrase).build();
wifiP2pManager.createGroup(channel, config, listener);
} else {
wifiP2pManager.createGroup(channel, listener);
}
} catch (SecurityException e) {
releaseWifiP2pHotspot(
app.getString(R.string.callback_permission_denied));
}
}
private void requestGroupInfo(int attempt, @Nullable String networkName) {
Log.e("TEST", "requestGroupInfo attempt: " + attempt);
GroupInfoListener listener = group -> {
boolean retry = false;
if (group == null) retry = true;
else if (!group.getNetworkName().startsWith("DIRECT-") ||
(networkName != null &&
!networkName.equals(group.getNetworkName()))) {
// we only retry if we have attempts left, otherwise we try what we got
if (attempt < MAX_GROUP_INFO_ATTEMPTS) retry = true;
Log.e("TEST",
"received networkName: " + group.getNetworkName());
Log.e("TEST", "received passphrase: " + group.getPassphrase());
}
if (retry) {
// 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, networkName),
1000);
} else {
releaseWifiP2pHotspot(
app.getString(R.string.callback_no_group_info));
}
} else {
config.setValue(new NetworkConfig(group.getNetworkName(),
group.getPassphrase(),
true));
if (SDK_INT >= 29) {
double freq = ((double) group.getFrequency()) / 1000;
status.setValue(
app.getString(R.string.callback_started_freq,
freq));
} else {
status.setValue(app.getString(R.string.callback_started));
}
startWebServer();
}
};
try {
wifiP2pManager.requestGroupInfo(channel, listener);
} catch (SecurityException e) {
releaseWifiP2pHotspot(
app.getString(R.string.callback_permission_denied));
}
}
private void releaseWifiP2pHotspot(String statusMessage) {
stopWebServer();
if (SDK_INT >= 27) channel.close();
channel = null;
releaseLock();
config.setValue(null);
status.setValue(statusMessage);
HotspotManager getHotSpotManager() {
return hotSpotManager;
}
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onSuccess() {
releaseWifiP2pHotspot(app.getString(R.string.hotspot_stopped));
}
@Override
public void onFailure(int reason) {
releaseWifiP2pHotspot(app.getString(R.string.hotspot_stopped));
}
});
void startWifiP2pHotspot() {
hotSpotManager.startWifiP2pHotspot();
}
@Override
protected void onCleared() {
stopWifiP2pHotspot();
}
@SuppressLint("WakelockTimeout")
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 releaseLock() {
wifiLock.release();
hotSpotManager.stopWifiP2pHotspot();
}
private void startWebServer() {
......@@ -229,17 +75,18 @@ public class MainViewModel extends AndroidViewModel {
webServerState.postValue(WebServerState.STOPPED);
}
enum WebServerState {STOPPED, STARTED, ERROR}
@Override
public void connected() {
Log.e("TEST", "starting webserver");
startWebServer();
}
static class NetworkConfig {
@Override
public void disconnected() {
Log.e("TEST", "stopping webserver");
stopWebServer();
}
final String ssid, password;
final boolean hidden;
enum WebServerState {STOPPED, STARTED, ERROR}
NetworkConfig(String ssid, String password, boolean hidden) {
this.ssid = ssid;
this.password = password;
this.hidden = hidden;
}
}
}
......@@ -20,7 +20,7 @@
<string name="starting_hotspot">Starting hotspot</string>
<string name="callback_started">Hotspot started</string>
<string name="callback_started_freq">Hotspot started (%1$.2f GHz)</string>
<string name="callback_failed">Hotspot failed to start: error code %d</string>
<string name="callback_failed">Hotspot failed to start: error %s</string>
<string name="callback_waiting">Waiting for hotspot to start</string>
<string name="callback_no_group_info">Hotspot failed to start: no group info</string>
<string name="hotspot_stopped">Hotspot stopped</string>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment