diff --git a/app/src/androidTest/java/org/fdroid/fdroid/Netstat.java b/app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
new file mode 100644
index 000000000..7354e6843
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
@@ -0,0 +1,372 @@
+package org.fdroid.fdroid;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Replacer for the netstat utility, by reading the /proc filesystem it can find out the
+ * open connections of the system
+ * From http://www.ussg.iu.edu/hypermail/linux/kernel/0409.1/2166.html :
+ * It will first list all listening TCP sockets, and next list all established
+ * TCP connections. A typical entry of /proc/net/tcp would look like this (split
+ * up into 3 parts because of the length of the line):
+ *
+ * 46: 010310AC:9C4C 030310AC:1770 01
+ * | | | | | |--> connection state
+ * | | | | |------> remote TCP port number
+ * | | | |-------------> remote IPv4 address
+ * | | |--------------------> local TCP port number
+ * | |---------------------------> local IPv4 address
+ * |----------------------------------> number of entry
+ *
+ * 00000150:00000000 01:00000019 00000000
+ * | | | | |--> number of unrecovered RTO timeouts
+ * | | | |----------> number of jiffies until timer expires
+ * | | |----------------> timer_active (see below)
+ * | |----------------------> receive-queue
+ * |-------------------------------> transmit-queue
+ *
+ * 1000 0 54165785 4 cd1e6040 25 4 27 3 -1
+ * | | | | | | | | | |--> slow start size threshold,
+ * | | | | | | | | | or -1 if the treshold
+ * | | | | | | | | | is >= 0xFFFF
+ * | | | | | | | | |----> sending congestion window
+ * | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
+ * | | | | | | |---------> Predicted tick of soft clock
+ * | | | | | | (delayed ACK control data)
+ * | | | | | |------------> retransmit timeout
+ * | | | | |------------------> location of socket in memory
+ * | | | |-----------------------> socket reference count
+ * | | |-----------------------------> inode
+ * | |----------------------------------> unanswered 0-window probes
+ * |---------------------------------------------> uid
+ *
+ * @author Ciprian Dobre
+ */
+public class Netstat {
+
+ /**
+ * Possible values for states in /proc/net/tcp
+ */
+ private static final String[] STATES = {
+ "ESTBLSH", "SYNSENT", "SYNRECV", "FWAIT1", "FWAIT2", "TMEWAIT",
+ "CLOSED", "CLSWAIT", "LASTACK", "LISTEN", "CLOSING", "UNKNOWN",
+ };
+ /**
+ * Pattern used when parsing through /proc/net/tcp
+ */
+ private static final Pattern NET_PATTERN = Pattern.compile(
+ "\\d+:\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+):([\\dA-F]+)\\s+([\\dA-F]+)\\s+" +
+ "[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+:[\\dA-F]+\\s+[\\dA-F]+\\s+([\\d]+)\\s+[\\d]+\\s+([\\d]+)");
+
+ /**
+ * Utility method that converts an address from a hex representation as founded in /proc to String representation
+ */
+ private static String getAddress(final String hexa) {
+ try {
+ // first let's convert the address to Integer
+ final long v = Long.parseLong(hexa, 16);
+ // in /proc the order is little endian and java uses big endian order we also need to invert the order
+ final long adr = (v >>> 24) | (v << 24) |
+ ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
+ // and now it's time to output the result
+ return ((adr >> 24) & 0xff) + "." + ((adr >> 16) & 0xff) + "." + ((adr >> 8) & 0xff) + "." + (adr & 0xff);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return "0.0.0.0"; // NOPMD
+ }
+ }
+
+ private static int getInt16(final String hexa) {
+ try {
+ return Integer.parseInt(hexa, 16);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return -1;
+ }
+ }
+
+ /*
+ private static String getPName(final int pid) {
+ final Pattern pattern = Pattern.compile("Name:\\s*(\\S+)");
+ try {
+ BufferedReader in = new BufferedReader(new FileReader("/proc/" + pid + "/status"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ final Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ in.close();
+ } catch (Throwable t) {
+ // ignored
+ }
+ return "UNKNOWN";
+ }
+ */
+
+ /**
+ * Method used to question for the connections currently openned
+ *
+ * @return The list of connections (as Connection objects)
+ */
+ public static List getConnections() {
+
+ final ArrayList net = new ArrayList<>();
+
+ // read from /proc/net/tcp the list of currently openned socket connections
+ try {
+ BufferedReader in = new BufferedReader(new FileReader("/proc/net/tcp"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ Matcher matcher = NET_PATTERN.matcher(line);
+ if (matcher.find()) {
+ final Connection c = new Connection();
+ c.setProtocol(Connection.TCP_CONNECTION);
+ net.add(c);
+ final String localPortHexa = matcher.group(2);
+ final String remoteAddressHexa = matcher.group(3);
+ final String remotePortHexa = matcher.group(4);
+ final String statusHexa = matcher.group(5);
+ //final String uid = matcher.group(6);
+ //final String inode = matcher.group(7);
+ c.setLocalPort(getInt16(localPortHexa));
+ c.setRemoteAddress(getAddress(remoteAddressHexa));
+ c.setRemotePort(getInt16(remotePortHexa));
+ try {
+ c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
+ } catch (Exception ex) {
+ c.setStatus(STATES[11]); // unknwon
+ }
+ c.setPID(-1); // unknown
+ c.setPName("UNKNOWN");
+ }
+ }
+ in.close();
+ } catch (Throwable t) { // NOPMD
+ // ignored
+ }
+
+ // read from /proc/net/udp the list of currently openned socket connections
+ try {
+ BufferedReader in = new BufferedReader(new FileReader("/proc/net/udp"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ Matcher matcher = NET_PATTERN.matcher(line);
+ if (matcher.find()) {
+ final Connection c = new Connection();
+ c.setProtocol(Connection.UDP_CONNECTION);
+ net.add(c);
+ final String localPortHexa = matcher.group(2);
+ final String remoteAddressHexa = matcher.group(3);
+ final String remotePortHexa = matcher.group(4);
+ final String statusHexa = matcher.group(5);
+ //final String uid = matcher.group(6);
+ //final String inode = matcher.group(7);
+ c.setLocalPort(getInt16(localPortHexa));
+ c.setRemoteAddress(getAddress(remoteAddressHexa));
+ c.setRemotePort(getInt16(remotePortHexa));
+ try {
+ c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
+ } catch (Exception ex) {
+ c.setStatus(STATES[11]); // unknwon
+ }
+ c.setPID(-1); // unknown
+ c.setPName("UNKNOWN");
+ }
+ }
+ in.close();
+ } catch (Throwable t) { // NOPMD
+ // ignored
+ }
+
+ // read from /proc/net/raw the list of currently openned socket connections
+ try {
+ BufferedReader in = new BufferedReader(new FileReader("/proc/net/raw"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ Matcher matcher = NET_PATTERN.matcher(line);
+ if (matcher.find()) {
+ final Connection c = new Connection();
+ c.setProtocol(Connection.RAW_CONNECTION);
+ net.add(c);
+ //final String localAddressHexa = matcher.group(1);
+ final String localPortHexa = matcher.group(2);
+ final String remoteAddressHexa = matcher.group(3);
+ final String remotePortHexa = matcher.group(4);
+ final String statusHexa = matcher.group(5);
+ //final String uid = matcher.group(6);
+ //final String inode = matcher.group(7);
+ c.setLocalPort(getInt16(localPortHexa));
+ c.setRemoteAddress(getAddress(remoteAddressHexa));
+ c.setRemotePort(getInt16(remotePortHexa));
+ try {
+ c.setStatus(STATES[Integer.parseInt(statusHexa, 16) - 1]);
+ } catch (Exception ex) {
+ c.setStatus(STATES[11]); // unknwon
+ }
+ c.setPID(-1); // unknown
+ c.setPName("UNKNOWN");
+ }
+ }
+ in.close();
+ } catch (Throwable t) { // NOPMD
+ // ignored
+ }
+ return net;
+ }
+
+ /**
+ * Informations about a given connection
+ *
+ * @author Ciprian Dobre
+ */
+ public static class Connection {
+
+ /**
+ * Types of connection protocol
+ ***/
+ public static final byte TCP_CONNECTION = 0;
+ public static final byte UDP_CONNECTION = 1;
+ public static final byte RAW_CONNECTION = 2;
+ /**
+ * serialVersionUID
+ */
+ private static final long serialVersionUID = 1988671591829311032L;
+ /**
+ * The protocol of the connection (can be tcp, udp or raw)
+ */
+ protected byte protocol;
+
+ /**
+ * The owner of the connection (username)
+ */
+ protected String powner;
+
+ /**
+ * The pid of the owner process
+ */
+ protected int pid;
+
+ /**
+ * The name of the program owning the connection
+ */
+ protected String pname;
+
+ /**
+ * Local port
+ */
+ protected int localPort;
+
+ /**
+ * Remote address of the connection
+ */
+ protected String remoteAddress;
+
+ /**
+ * Remote port
+ */
+ protected int remotePort;
+
+ /**
+ * Status of the connection
+ */
+ protected String status;
+
+ public final byte getProtocol() {
+ return protocol;
+ }
+
+ public final void setProtocol(final byte protocol) {
+ this.protocol = protocol;
+ }
+
+ public final String getProtocolAsString() {
+ switch (protocol) {
+ case TCP_CONNECTION:
+ return "TCP";
+ case UDP_CONNECTION:
+ return "UDP";
+ case RAW_CONNECTION:
+ return "RAW";
+ }
+ return "UNKNOWN";
+ }
+
+ public final String getPOwner() {
+ return powner;
+ }
+
+ public final void setPOwner(final String owner) {
+ this.powner = owner;
+ }
+
+ public final int getPID() {
+ return pid;
+ }
+
+ public final void setPID(final int pid) {
+ this.pid = pid;
+ }
+
+ public final String getPName() {
+ return pname;
+ }
+
+ public final void setPName(final String pname) {
+ this.pname = pname;
+ }
+
+ public final int getLocalPort() {
+ return localPort;
+ }
+
+ public final void setLocalPort(final int localPort) {
+ this.localPort = localPort;
+ }
+
+ public final String getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public final void setRemoteAddress(final String remoteAddress) {
+ this.remoteAddress = remoteAddress;
+ }
+
+ public final int getRemotePort() {
+ return remotePort;
+ }
+
+ public final void setRemotePort(final int remotePort) {
+ this.remotePort = remotePort;
+ }
+
+ public final String getStatus() {
+ return status;
+ }
+
+ public final void setStatus(final String status) {
+ this.status = status;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("[Prot=").append(getProtocolAsString());
+ buf.append(",POwner=").append(powner);
+ buf.append(",PID=").append(pid);
+ buf.append(",PName=").append(pname);
+ buf.append(",LPort=").append(localPort);
+ buf.append(",RAddress=").append(remoteAddress);
+ buf.append(",RPort=").append(remotePort);
+ buf.append(",Status=").append(status);
+ buf.append("]");
+ return buf.toString();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java b/app/src/androidTest/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java
new file mode 100644
index 000000000..8933ba73a
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java
@@ -0,0 +1,187 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Netstat;
+import org.fdroid.fdroid.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(AndroidJUnit4.class)
+public class LocalHTTPDManagerTest {
+ private static final String TAG = "LocalHTTPDManagerTest";
+
+ private Context context;
+ private LocalBroadcastManager lbm;
+
+ private static final String LOCALHOST = "localhost";
+ private static final int PORT = 8888;
+
+ @Before
+ public void setUp() {
+ context = InstrumentationRegistry.getTargetContext();
+ lbm = LocalBroadcastManager.getInstance(context);
+
+ FDroidApp.ipAddressString = LOCALHOST;
+ FDroidApp.port = PORT;
+
+ for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
+ Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
+ }
+ assertFalse(Utils.isServerSocketInUse(PORT));
+ LocalHTTPDManager.stop(context);
+
+ for (Netstat.Connection connection : Netstat.getConnections()) { // NOPMD
+ Log.i("LocalHTTPDManagerTest", "connection: " + connection.toString());
+ }
+ }
+
+ @After
+ public void tearDown() {
+ lbm.unregisterReceiver(startedReceiver);
+ lbm.unregisterReceiver(stoppedReceiver);
+ lbm.unregisterReceiver(errorReceiver);
+ }
+
+ @Test
+ public void testStartStop() throws InterruptedException {
+ Log.i(TAG, "testStartStop");
+
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ BroadcastReceiver latchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ startLatch.countDown();
+ }
+ };
+ lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
+ lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
+ lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
+ LocalHTTPDManager.start(context, false);
+ assertTrue(startLatch.await(30, TimeUnit.SECONDS));
+ assertTrue(Utils.isServerSocketInUse(PORT));
+ assertTrue(Utils.canConnectToSocket(LOCALHOST, PORT));
+ lbm.unregisterReceiver(latchReceiver);
+ lbm.unregisterReceiver(stoppedReceiver);
+ lbm.unregisterReceiver(errorReceiver);
+
+ final CountDownLatch stopLatch = new CountDownLatch(1);
+ latchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ stopLatch.countDown();
+ }
+ };
+ lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
+ lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
+ lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
+ LocalHTTPDManager.stop(context);
+ assertTrue(stopLatch.await(30, TimeUnit.SECONDS));
+ assertFalse(Utils.isServerSocketInUse(PORT));
+ assertFalse(Utils.canConnectToSocket(LOCALHOST, PORT)); // if this is flaky, just remove it
+ lbm.unregisterReceiver(latchReceiver);
+ }
+
+ @Test
+ public void testError() throws InterruptedException, IOException {
+ Log.i("LocalHTTPDManagerTest", "testError");
+ ServerSocket blockerSocket = new ServerSocket(PORT);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ BroadcastReceiver latchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ latch.countDown();
+ }
+ };
+ lbm.registerReceiver(startedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
+ lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
+ lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
+ LocalHTTPDManager.start(context, false);
+ assertTrue(latch.await(30, TimeUnit.SECONDS));
+ assertTrue(Utils.isServerSocketInUse(PORT));
+ assertNotEquals(PORT, FDroidApp.port);
+ assertFalse(Utils.isServerSocketInUse(FDroidApp.port));
+ lbm.unregisterReceiver(latchReceiver);
+ blockerSocket.close();
+ }
+
+ @Test
+ public void testRestart() throws InterruptedException, IOException {
+ Log.i("LocalHTTPDManagerTest", "testRestart");
+ assertFalse(Utils.isServerSocketInUse(PORT));
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ BroadcastReceiver latchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ startLatch.countDown();
+ }
+ };
+ lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
+ lbm.registerReceiver(stoppedReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STOPPED));
+ lbm.registerReceiver(errorReceiver, new IntentFilter(LocalHTTPDManager.ACTION_ERROR));
+ LocalHTTPDManager.start(context, false);
+ assertTrue(startLatch.await(30, TimeUnit.SECONDS));
+ assertTrue(Utils.isServerSocketInUse(PORT));
+ lbm.unregisterReceiver(latchReceiver);
+ lbm.unregisterReceiver(stoppedReceiver);
+
+ final CountDownLatch restartLatch = new CountDownLatch(1);
+ latchReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ restartLatch.countDown();
+ }
+ };
+ lbm.registerReceiver(latchReceiver, new IntentFilter(LocalHTTPDManager.ACTION_STARTED));
+ LocalHTTPDManager.restart(context, false);
+ assertTrue(restartLatch.await(30, TimeUnit.SECONDS));
+ lbm.unregisterReceiver(latchReceiver);
+ }
+
+ private final BroadcastReceiver startedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String message = intent.getStringExtra(Intent.EXTRA_TEXT);
+ Log.i(TAG, "startedReceiver: " + message);
+ fail();
+ }
+ };
+
+ private final BroadcastReceiver stoppedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String message = intent.getStringExtra(Intent.EXTRA_TEXT);
+ Log.i(TAG, "stoppedReceiver: " + message);
+ fail();
+ }
+ };
+
+ private final BroadcastReceiver errorReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String message = intent.getStringExtra(Intent.EXTRA_TEXT);
+ Log.i(TAG, "errorReceiver: " + message);
+ fail();
+ }
+ };
+}
diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml
index 55ef16a6e..a39ec62e8 100644
--- a/app/src/full/AndroidManifest.xml
+++ b/app/src/full/AndroidManifest.xml
@@ -78,6 +78,7 @@
android:exported="false"/>
+
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalHTTPDManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalHTTPDManager.java
new file mode 100644
index 000000000..eabbd0a46
--- /dev/null
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalHTTPDManager.java
@@ -0,0 +1,129 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.net.LocalHTTPD;
+import org.fdroid.fdroid.net.WifiStateChangeService;
+
+import java.io.IOException;
+import java.net.BindException;
+import java.util.Random;
+
+/**
+ * Manage {@link LocalHTTPD} in a {@link HandlerThread};
+ */
+public class LocalHTTPDManager {
+ private static final String TAG = "LocalHTTPDManager";
+
+ public static final String ACTION_STARTED = "LocalHTTPDStarted";
+ public static final String ACTION_STOPPED = "LocalHTTPDStopped";
+ public static final String ACTION_ERROR = "LocalHTTPDError";
+
+ private static final int STOP = 5709;
+
+ private static Handler handler;
+ private static volatile HandlerThread handlerThread;
+ private static LocalHTTPD localHttpd;
+
+ public static void start(Context context) {
+ start(context, Preferences.get().isLocalRepoHttpsEnabled());
+ }
+
+ /**
+ * Testable version, not for regular use.
+ *
+ * @see #start(Context)
+ */
+ static void start(final Context context, final boolean useHttps) {
+ if (handlerThread != null && handlerThread.isAlive()) {
+ Log.w(TAG, "handlerThread is already running, doing nothing!");
+ return;
+ }
+
+ handlerThread = new HandlerThread("LocalHTTPD", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
+ @Override
+ protected void onLooperPrepared() {
+ localHttpd = new LocalHTTPD(
+ context,
+ FDroidApp.ipAddressString,
+ FDroidApp.port,
+ context.getFilesDir(),
+ useHttps);
+ try {
+ localHttpd.start();
+ Intent intent = new Intent(ACTION_STARTED);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ } catch (BindException e) {
+ int prev = FDroidApp.port;
+ FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
+ WifiStateChangeService.start(context, null);
+ Intent intent = new Intent(ACTION_ERROR);
+ intent.putExtra(Intent.EXTRA_TEXT,
+ "port " + prev + " occupied, trying on " + FDroidApp.port + ": ("
+ + e.getLocalizedMessage() + ")");
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Intent intent = new Intent(ACTION_ERROR);
+ intent.putExtra(Intent.EXTRA_TEXT, e.getLocalizedMessage());
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+ }
+ };
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ localHttpd.stop();
+ handlerThread.quit();
+ handlerThread = null;
+ }
+ };
+ }
+
+ public static void stop(Context context) {
+ if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
+ Log.w(TAG, "handlerThread is already stopped, doing nothing!");
+ handlerThread = null;
+ return;
+ }
+ handler.sendEmptyMessage(STOP);
+ Intent stoppedIntent = new Intent(ACTION_STOPPED);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(stoppedIntent);
+ }
+
+ /**
+ * Run {@link #stop(Context)}, wait for it to actually stop, then run
+ * {@link #start(Context)}.
+ */
+ public static void restart(Context context) {
+ restart(context, Preferences.get().isLocalRepoHttpsEnabled());
+ }
+
+ /**
+ * Testable version, not for regular use.
+ *
+ * @see #restart(Context)
+ */
+ static void restart(Context context, boolean useHttps) {
+ stop(context);
+ try {
+ handlerThread.join(10000);
+ } catch (InterruptedException | NullPointerException e) {
+ // ignored
+ }
+ start(context, useHttps);
+ }
+
+ public static boolean isAlive() {
+ return handlerThread != null && handlerThread.isAlive();
+ }
+}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java
index f8675347c..36c0ee1e2 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java
@@ -133,23 +133,17 @@ public class SwapService extends Service {
if (getPeer() == null) {
throw new IllegalStateException("Cannot connect to peer, no peer has been selected.");
}
- connectTo(getPeer(), getPeer().shouldPromptForSwapBack());
+ connectTo(getPeer());
+ if (isEnabled() && getPeer().shouldPromptForSwapBack()) {
+ askServerToSwapWithUs(peerRepo);
+ }
}
- public void connectTo(@NonNull Peer peer, boolean requestSwapBack) {
+ public void connectTo(@NonNull Peer peer) {
if (peer != this.peer) {
Log.e(TAG, "Oops, got a different peer to swap with than initially planned.");
}
-
peerRepo = ensureRepoExists(peer);
-
- // Only ask server to swap with us, if we are actually running a local repo service.
- // It is possible to have a swap initiated without first starting a swap, in which
- // case swapping back is pointless.
- if (isEnabled() && requestSwapBack) {
- askServerToSwapWithUs(peerRepo);
- }
-
UpdateService.updateRepoNow(this, peer.getRepoAddress());
}
@@ -184,7 +178,9 @@ public class SwapService extends Service {
intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage());
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
} finally {
- conn.disconnect();
+ if (conn != null) {
+ conn.disconnect();
+ }
}
return null;
}
@@ -362,7 +358,7 @@ public class SwapService extends Service {
}
public boolean isEnabled() {
- return bluetoothSwap.isConnected() || wifiSwap.isConnected();
+ return bluetoothSwap.isConnected() || LocalHTTPDManager.isAlive();
}
// ==========================================
@@ -492,6 +488,7 @@ public class SwapService extends Service {
bluetoothAdapter.disable();
}
+ LocalHTTPDManager.stop(this);
if (wifiManager != null && !wasWifiEnabledBeforeSwap()) {
wifiManager.setWifiEnabled(false);
}
@@ -548,7 +545,7 @@ public class SwapService extends Service {
@Override
public void run() {
if (peer != null) {
- connectTo(peer, false);
+ connectTo(peer);
}
}
};
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java b/app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
index f76dbe042..efec3e91e 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
@@ -1,18 +1,12 @@
package org.fdroid.fdroid.localrepo.type;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.net.wifi.WifiManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.LocalHTTPDManager;
import org.fdroid.fdroid.localrepo.SwapService;
-import org.fdroid.fdroid.net.LocalHTTPD;
-import org.fdroid.fdroid.net.WifiStateChangeService;
import rx.Single;
import rx.SingleSubscriber;
import rx.android.schedulers.AndroidSchedulers;
@@ -20,17 +14,11 @@ import rx.functions.Action1;
import rx.functions.Func2;
import rx.schedulers.Schedulers;
-import java.io.IOException;
-import java.net.BindException;
-import java.util.Random;
-
@SuppressWarnings("LineLength")
public class WifiSwap extends SwapType {
private static final String TAG = "WifiSwap";
- private Handler webServerThreadHandler;
- private LocalHTTPD localHttpd;
private final BonjourBroadcast bonjourBroadcast;
private final WifiManager wifiManager;
@@ -50,10 +38,10 @@ public class WifiSwap extends SwapType {
@Override
public void start() {
+ sendBroadcast(SwapService.EXTRA_STARTING);
wifiManager.setWifiEnabled(true);
- Utils.debugLog(TAG, "Preparing swap webserver.");
- sendBroadcast(SwapService.EXTRA_STARTING);
+ LocalHTTPDManager.start(context);
if (FDroidApp.ipAddressString == null) {
Log.e(TAG, "Not starting swap webserver, because we don't seem to be connected to a network.");
@@ -110,65 +98,17 @@ public class WifiSwap extends SwapType {
private Single.OnSubscribe getWebServerTask() {
return new Single.OnSubscribe() {
@Override
- public void call(final SingleSubscriber super Boolean> singleSubscriber) {
- new Thread(new Runnable() {
- // Tell Eclipse this is not a leak because of Looper use.
- @SuppressLint("HandlerLeak")
- @Override
- public void run() {
- localHttpd = new LocalHTTPD(
- context,
- FDroidApp.ipAddressString,
- FDroidApp.port,
- context.getFilesDir(),
- Preferences.get().isLocalRepoHttpsEnabled());
-
- Looper.prepare(); // must be run before creating a Handler
- webServerThreadHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
- localHttpd.stop();
- Looper looper = Looper.myLooper();
- if (looper == null) {
- Log.e(TAG, "Looper.myLooper() was null for sum reason while shutting down the swap webserver.");
- } else {
- looper.quit();
- }
- }
- };
- try {
- Utils.debugLog(TAG, "Starting swap webserver...");
- localHttpd.start();
- Utils.debugLog(TAG, "Swap webserver started.");
- singleSubscriber.onSuccess(true);
- } catch (BindException e) {
- int prev = FDroidApp.port;
- FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
- WifiStateChangeService.start(context, null);
- singleSubscriber.onError(new Exception("port " + prev + " occupied, trying on " + FDroidApp.port + "!"));
- } catch (IOException e) {
- Log.e(TAG, "Could not start local repo HTTP server", e);
- singleSubscriber.onError(e);
- }
- Looper.loop(); // start the message receiving loop
- }
- }).start();
+ public void call(SingleSubscriber super Boolean> singleSubscriber) {
+ singleSubscriber.onSuccess(true);
}
};
}
@Override
public void stop() {
- sendBroadcast(SwapService.EXTRA_STOPPING);
- if (webServerThreadHandler == null) {
- Log.i(TAG, "null handler in stopWebServer");
- } else {
- Utils.debugLog(TAG, "Sending message to swap webserver to stop it.");
- Message msg = webServerThreadHandler.obtainMessage();
- msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
- webServerThreadHandler.sendMessage(msg);
- }
+ sendBroadcast(SwapService.EXTRA_STOPPING); // This needs to be per-SwapType
+ Utils.debugLog(TAG, "Sending message to swap webserver to stop it.");
+ LocalHTTPDManager.stop(context);
// Stop the Bonjour stuff after asking the webserver to stop. This is not required in this
// order, but it helps. In practice, the Bonjour stuff takes a second or two to stop. This
diff --git a/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java b/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
index 8315909ee..beaf2b4c0 100644
--- a/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
+++ b/app/src/full/java/org/fdroid/fdroid/net/LocalHTTPD.java
@@ -49,6 +49,7 @@ import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -80,7 +81,7 @@ public class LocalHTTPD extends NanoHTTPD {
*/
public static final String[] INDEX_FILE_NAMES = {"index.html"};
- private final Context context;
+ private final WeakReference context;
protected List rootDirs;
@@ -101,7 +102,7 @@ public class LocalHTTPD extends NanoHTTPD {
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
super(hostname, port);
rootDirs = Collections.singletonList(webRoot);
- this.context = context.getApplicationContext();
+ this.context = new WeakReference<>(context.getApplicationContext());
if (useHttps) {
enableHTTPS();
}
@@ -370,7 +371,7 @@ public class LocalHTTPD extends NanoHTTPD {
return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT,
"Requires 'repo' parameter to be posted.");
}
- SwapWorkflowActivity.requestSwap(context, session.getParms().get("repo"));
+ SwapWorkflowActivity.requestSwap(context.get(), session.getParms().get("repo"));
return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Swap request received.");
}
return newFixedLengthResponse("");
@@ -491,7 +492,7 @@ public class LocalHTTPD extends NanoHTTPD {
private void enableHTTPS() {
try {
- LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+ LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context.get());
SSLServerSocketFactory factory = NanoHTTPD.makeSSLSocketFactory(
localRepoKeyStore.getKeyStore(),
localRepoKeyStore.getKeyManagers());
diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index b6121dd08..58611ba37 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -63,6 +63,9 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -887,4 +890,26 @@ public final class Utils {
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
swipeLayout.setColorSchemeColors(typedValue.data);
}
+
+ public static boolean canConnectToSocket(String host, int port) {
+ try {
+ Socket socket = new Socket();
+ socket.connect(new InetSocketAddress(host, port), 5);
+ socket.close();
+ return true;
+ } catch (IOException e) {
+ // Could not connect.
+ return false;
+ }
+ }
+
+ public static boolean isServerSocketInUse(int port) {
+ try {
+ (new ServerSocket(port)).close();
+ return false;
+ } catch (IOException e) {
+ // Could not connect.
+ return true;
+ }
+ }
}
diff --git a/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java b/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java
new file mode 100644
index 000000000..6326e38ad
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java
@@ -0,0 +1,72 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.Context;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Utils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLog;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+
+@RunWith(RobolectricTestRunner.class)
+public class LocalHTTPDManagerTest {
+
+ @Test
+ public void testStartStop() throws InterruptedException {
+ ShadowLog.stream = System.out;
+ Context context = RuntimeEnvironment.application;
+
+ final String host = "localhost";
+ final int port = 8888;
+ assertFalse(Utils.isServerSocketInUse(port));
+ LocalHTTPDManager.stop(context);
+
+ FDroidApp.ipAddressString = host;
+ FDroidApp.port = port;
+
+ LocalHTTPDManager.start(context, false);
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ while (!Utils.isServerSocketInUse(port)) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ }
+ startLatch.countDown();
+ }
+ }).start();
+ assertTrue(startLatch.await(30, TimeUnit.SECONDS));
+ assertTrue(Utils.isServerSocketInUse(port));
+ assertTrue(Utils.canConnectToSocket(host, port));
+
+ LocalHTTPDManager.stop(context);
+ final CountDownLatch stopLatch = new CountDownLatch(1);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ while (!Utils.isServerSocketInUse(port)) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ fail();
+ }
+ }
+ stopLatch.countDown();
+ }
+ }).start();
+ assertTrue(stopLatch.await(10, TimeUnit.SECONDS));
+ }
+}