diff --git a/app/build.gradle b/app/build.gradle
index 2b7d709d6..47fa26d11 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -153,7 +153,6 @@ dependencies {
implementation 'commons-net:commons-net:3.6'
implementation 'ch.acra:acra:4.9.1'
implementation 'io.reactivex:rxjava:1.1.0'
- implementation 'io.reactivex:rxandroid:0.23.0'
implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1'
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5'
diff --git a/app/lint.xml b/app/lint.xml
index 9b706ddf7..bab976a18 100644
--- a/app/lint.xml
+++ b/app/lint.xml
@@ -40,6 +40,8 @@
+
+
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/BonjourManagerTest.java b/app/src/androidTest/java/org/fdroid/fdroid/localrepo/BonjourManagerTest.java
new file mode 100644
index 000000000..71eaa653d
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/localrepo/BonjourManagerTest.java
@@ -0,0 +1,125 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import org.fdroid.fdroid.FDroidApp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import javax.jmdns.ServiceEvent;
+import javax.jmdns.ServiceListener;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class BonjourManagerTest {
+
+ private static final String NAME = "Robolectric-test";
+ private static final String LOCALHOST = "localhost";
+ private static final int PORT = 8888;
+
+ @Test
+ public void testStartStop() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+
+ FDroidApp.ipAddressString = LOCALHOST;
+ FDroidApp.port = PORT;
+
+ final CountDownLatch addedLatch = new CountDownLatch(1);
+ final CountDownLatch resolvedLatch = new CountDownLatch(1);
+ final CountDownLatch removedLatch = new CountDownLatch(1);
+ BonjourManager.start(context, NAME, false,
+ new ServiceListener() {
+ @Override
+ public void serviceAdded(ServiceEvent serviceEvent) {
+ System.out.println("Service added: " + serviceEvent.getInfo());
+ if (NAME.equals(serviceEvent.getName())) {
+ addedLatch.countDown();
+ }
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent serviceEvent) {
+ System.out.println("Service removed: " + serviceEvent.getInfo());
+ removedLatch.countDown();
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent serviceEvent) {
+ System.out.println("Service resolved: " + serviceEvent.getInfo());
+ if (NAME.equals(serviceEvent.getName())) {
+ resolvedLatch.countDown();
+ }
+ }
+ }, getBlankServiceListener());
+ BonjourManager.setVisible(context, true);
+ assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
+ assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
+ BonjourManager.setVisible(context, false);
+ assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
+ BonjourManager.stop(context);
+ }
+
+ @Test
+ public void testRestart() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+
+ FDroidApp.ipAddressString = LOCALHOST;
+ FDroidApp.port = PORT;
+
+ BonjourManager.start(context, NAME, false, getBlankServiceListener(), getBlankServiceListener());
+
+ final CountDownLatch addedLatch = new CountDownLatch(1);
+ final CountDownLatch resolvedLatch = new CountDownLatch(1);
+ final CountDownLatch removedLatch = new CountDownLatch(1);
+ BonjourManager.restart(context, NAME, false,
+ new ServiceListener() {
+ @Override
+ public void serviceAdded(ServiceEvent serviceEvent) {
+ System.out.println("Service added: " + serviceEvent.getInfo());
+ if (NAME.equals(serviceEvent.getName())) {
+ addedLatch.countDown();
+ }
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent serviceEvent) {
+ System.out.println("Service removed: " + serviceEvent.getInfo());
+ removedLatch.countDown();
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent serviceEvent) {
+ System.out.println("Service resolved: " + serviceEvent.getInfo());
+ if (NAME.equals(serviceEvent.getName())) {
+ resolvedLatch.countDown();
+ }
+ }
+ }, getBlankServiceListener());
+ BonjourManager.setVisible(context, true);
+ assertTrue(addedLatch.await(30, TimeUnit.SECONDS));
+ assertTrue(resolvedLatch.await(30, TimeUnit.SECONDS));
+ BonjourManager.setVisible(context, false);
+ assertTrue(removedLatch.await(30, TimeUnit.SECONDS));
+ BonjourManager.stop(context);
+ }
+
+ private ServiceListener getBlankServiceListener() {
+ return new ServiceListener() {
+ @Override
+ public void serviceAdded(ServiceEvent serviceEvent) {
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent serviceEvent) {
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent serviceEvent) {
+ }
+ };
+ }
+}
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..ca4155de9
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/localrepo/LocalHTTPDManagerTest.java
@@ -0,0 +1,189 @@
+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.filters.LargeTest;
+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;
+
+@LargeTest
+@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/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
new file mode 100644
index 000000000..9b66614ac
--- /dev/null
+++ b/app/src/androidTest/java/org/fdroid/fdroid/updater/SwapRepoEmulatorTest.java
@@ -0,0 +1,201 @@
+package org.fdroid.fdroid.updater;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.text.TextUtils;
+import android.util.Log;
+import org.fdroid.fdroid.BuildConfig;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.IndexUpdater;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.data.Schema;
+import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
+import org.fdroid.fdroid.net.LocalHTTPD;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+import java.security.cert.Certificate;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+public class SwapRepoEmulatorTest {
+ public static final String TAG = "SwapRepoEmulatorTest";
+
+ /**
+ * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run()
+ */
+ @Test
+ public void testSwap()
+ throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
+ Looper.prepare();
+ LocalHTTPD localHttpd = null;
+ try {
+ Log.i(TAG, "REPO: " + FDroidApp.repo);
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ Preferences.setupForTests(context);
+
+ FDroidApp.initWifiSettings();
+ assertNull(FDroidApp.repo.address);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ new Thread() {
+ @Override
+ public void run() {
+ while (FDroidApp.repo.address == null) {
+ try {
+ Log.i(TAG, "Waiting for IP address... " + FDroidApp.repo.address);
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+ latch.countDown();
+ }
+ }.start();
+ latch.await(10, TimeUnit.MINUTES);
+ assertNotNull(FDroidApp.repo.address);
+
+ LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
+ Log.i(TAG, "REPO: " + FDroidApp.repo);
+ File indexJarFile = LocalRepoManager.get(context).getIndexJar();
+ assertTrue(indexJarFile.isFile());
+
+ localHttpd = new LocalHTTPD(
+ context,
+ null,
+ FDroidApp.port,
+ LocalRepoManager.get(context).getWebRoot(),
+ false);
+ localHttpd.start();
+ Thread.sleep(100); // give the server some tine to start.
+ assertTrue(localHttpd.isAlive());
+
+ LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+ Certificate localCert = localRepoKeyStore.getCertificate();
+ String signingCert = Hasher.hex(localCert);
+ assertFalse(TextUtils.isEmpty(signingCert));
+ assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
+
+ Repo repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+ while (repoToDelete != null) {
+ Log.d(TAG, "Removing old test swap repo matching this one: " + repoToDelete.address);
+ RepoProvider.Helper.remove(context, repoToDelete.getId());
+ repoToDelete = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+ }
+
+ ContentValues values = new ContentValues(4);
+ values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert);
+ values.put(Schema.RepoTable.Cols.ADDRESS, FDroidApp.repo.address);
+ values.put(Schema.RepoTable.Cols.NAME, FDroidApp.repo.name);
+ values.put(Schema.RepoTable.Cols.IS_SWAP, true);
+ final String lastEtag = UUID.randomUUID().toString();
+ values.put(Schema.RepoTable.Cols.LAST_ETAG, lastEtag);
+ RepoProvider.Helper.insert(context, values);
+ Repo repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+ assertTrue(repo.isSwap);
+ assertNotEquals(-1, repo.getId());
+ assertTrue(repo.name.startsWith(FDroidApp.repo.name));
+ assertEquals(lastEtag, repo.lastetag);
+ assertNull(repo.lastUpdated);
+
+ assertTrue(isPortInUse(FDroidApp.ipAddressString, FDroidApp.port));
+ Thread.sleep(100);
+ IndexUpdater updater = new IndexUpdater(context, repo);
+ updater.update();
+ assertTrue(updater.hasChanged());
+
+ repo = RepoProvider.Helper.findByAddress(context, FDroidApp.repo.address);
+ final Date lastUpdated = repo.lastUpdated;
+ assertTrue("repo lastUpdated should be updated", new Date(2019, 5, 13).compareTo(repo.lastUpdated) > 0);
+
+ App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(),
+ context.getPackageName(), repo.getId());
+ assertEquals(context.getPackageName(), app.packageName);
+
+ List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
+ assertEquals(1, apks.size());
+ for (Apk apk : apks) {
+ Log.i(TAG, "Apk: " + apk);
+ assertEquals(context.getPackageName(), apk.packageName);
+ assertEquals(BuildConfig.VERSION_NAME, apk.versionName);
+ assertEquals(BuildConfig.VERSION_CODE, apk.versionCode);
+ assertEquals(app.repoId, apk.repoId);
+ }
+
+ Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
+ mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ List resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
+ HashSet packageNames = new HashSet<>();
+ for (ResolveInfo resolveInfo : resolveInfoList) {
+ if (!isSystemPackage(resolveInfo)) {
+ Log.i(TAG, "resolveInfo: " + resolveInfo);
+ packageNames.add(resolveInfo.activityInfo.packageName);
+ }
+ }
+ LocalRepoService.runProcess(context, packageNames.toArray(new String[0]));
+
+ updater = new IndexUpdater(context, repo);
+ updater.update();
+ assertTrue(updater.hasChanged());
+ assertTrue("repo lastUpdated should be updated", lastUpdated.compareTo(repo.lastUpdated) < 0);
+
+ for (String packageName : packageNames) {
+ assertNotNull(ApkProvider.Helper.findByPackageName(context, packageName));
+ }
+ } finally {
+ if (localHttpd != null) {
+ localHttpd.stop();
+ }
+ }
+ if (localHttpd != null) {
+ assertFalse(localHttpd.isAlive());
+ }
+ }
+
+ private boolean isPortInUse(String host, int port) {
+ boolean result = false;
+
+ try {
+ (new Socket(host, port)).close();
+ result = true;
+ } catch (IOException e) {
+ // Could not connect.
+ e.printStackTrace();
+ }
+ return result;
+ }
+
+ private boolean isSystemPackage(ResolveInfo resolveInfo) {
+ return (resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ }
+}
diff --git a/app/src/basic/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/basic/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 4e86467a5..1dcfaf9bd 100644
--- a/app/src/basic/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/app/src/basic/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -19,10 +19,15 @@
package org.fdroid.fdroid.views.swap;
+import android.content.Context;
+import android.net.Uri;
+
/**
* Dummy version for basic app flavor.
*/
public class SwapWorkflowActivity {
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM";
+ public static void requestSwap(Context context, Uri uri) {
+ };
}
diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml
index 70c2bbe0f..a39ec62e8 100644
--- a/app/src/full/AndroidManifest.xml
+++ b/app/src/full/AndroidManifest.xml
@@ -52,6 +52,7 @@
android:label="@string/swap"
android:name=".views.swap.SwapWorkflowActivity"
android:parentActivityName=".views.main.MainActivity"
+ android:launchMode="singleTask"
android:theme="@style/SwapTheme.Wizard"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden">
@@ -77,8 +78,9 @@
android:exported="false"/>
+ context;
+ private static Handler handler;
+ private static volatile HandlerThread handlerThread;
+ private static BluetoothAdapter bluetoothAdapter;
+
+ /**
+ * Stops the Bluetooth adapter, triggering a status broadcast via {@link #ACTION_STATUS}.
+ * {@link #STATUS_STOPPED} can be broadcast multiple times for the same session,
+ * so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
+ */
+ public static void stop(Context context) {
+ BluetoothManager.context = new WeakReference<>(context);
+ if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
+ Log.w(TAG, "handlerThread is already stopped, doing nothing!");
+ sendBroadcast(STATUS_STOPPED, null);
+ return;
+ }
+ sendBroadcast(STATUS_STOPPING, null);
+ handler.sendEmptyMessage(STOP);
+ }
+
+ /**
+ * Starts the service, triggering a status broadcast via {@link #ACTION_STATUS}.
+ * {@link #STATUS_STARTED} can be broadcast multiple times for the same session,
+ * so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
+ */
+ public static void start(final Context context) {
+ BluetoothManager.context = new WeakReference<>(context);
+ if (handlerThread != null && handlerThread.isAlive()) {
+ sendBroadcast(STATUS_STARTED, null);
+ return;
+ }
+ sendBroadcast(STATUS_STARTING, null);
+
+ final BluetoothServer bluetoothServer = new BluetoothServer(context.getFilesDir());
+ handlerThread = new HandlerThread("BluetoothManager", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
+ @Override
+ protected void onLooperPrepared() {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
+ localBroadcastManager.registerReceiver(bluetoothDeviceFound,
+ new IntentFilter(BluetoothDevice.ACTION_FOUND));
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ String name = bluetoothAdapter.getName();
+ if (name != null) {
+ SwapService.putBluetoothNameBeforeSwap(name);
+ }
+ if (!bluetoothAdapter.enable()) {
+ sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth));
+ return;
+ }
+ bluetoothServer.start();
+ if (bluetoothAdapter.startDiscovery()) {
+ sendBroadcast(STATUS_STARTED, null);
+ } else {
+ sendBroadcast(STATUS_ERROR, context.getString(R.string.swap_error_cannot_start_bluetooth));
+ }
+ for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
+ sendFoundBroadcast(context, device);
+ }
+ }
+ };
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
+ localBroadcastManager.unregisterReceiver(bluetoothDeviceFound);
+ bluetoothServer.close();
+ if (bluetoothAdapter != null) {
+ bluetoothAdapter.cancelDiscovery();
+ if (!SwapService.wasBluetoothEnabledBeforeSwap()) {
+ bluetoothAdapter.disable();
+ }
+ String name = SwapService.getBluetoothNameBeforeSwap();
+ if (name != null) {
+ bluetoothAdapter.setName(name);
+ }
+ }
+ handlerThread.quit();
+ handlerThread = null;
+ sendBroadcast(STATUS_STOPPED, null);
+ }
+ };
+ }
+
+ public static void restart(Context context) {
+ stop(context);
+ try {
+ handlerThread.join(10000);
+ } catch (InterruptedException | NullPointerException e) {
+ // ignored
+ }
+ start(context);
+ }
+
+ public static void setName(Context context, String name) {
+ // TODO
+ }
+
+ public static boolean isAlive() {
+ return handlerThread != null && handlerThread.isAlive();
+ }
+
+ private static void sendBroadcast(int status, String message) {
+
+ Intent intent = new Intent(ACTION_STATUS);
+ intent.putExtra(EXTRA_STATUS, status);
+ if (!TextUtils.isEmpty(message)) {
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ }
+ LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
+ }
+
+ private static final BroadcastReceiver bluetoothDeviceFound = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ sendFoundBroadcast(context, (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ }
+ };
+
+ private static void sendFoundBroadcast(Context context, BluetoothDevice device) {
+ BluetoothPeer bluetoothPeer = BluetoothPeer.getInstance(device);
+ if (bluetoothPeer == null) {
+ Utils.debugLog(TAG, "IGNORING: " + device);
+ return;
+ }
+ Intent intent = new Intent(ACTION_FOUND);
+ intent.putExtra(EXTRA_PEER, bluetoothPeer);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/BonjourManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/BonjourManager.java
new file mode 100644
index 000000000..e579a65c2
--- /dev/null
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/BonjourManager.java
@@ -0,0 +1,284 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.support.v4.content.LocalBroadcastManager;
+import android.text.TextUtils;
+import android.util.Log;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.peers.BonjourPeer;
+
+import javax.jmdns.JmDNS;
+import javax.jmdns.ServiceEvent;
+import javax.jmdns.ServiceInfo;
+import javax.jmdns.ServiceListener;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.InetAddress;
+import java.util.HashMap;
+
+/**
+ * Manage {@link JmDNS} in a {@link HandlerThread}. The start process is in
+ * {@link HandlerThread#onLooperPrepared()} so that it is always started before
+ * any messages get delivered from the queue.
+ */
+public class BonjourManager {
+ private static final String TAG = "BonjourManager";
+
+ public static final String ACTION_FOUND = "BonjourNewPeer";
+ public static final String ACTION_REMOVED = "BonjourPeerRemoved";
+ public static final String EXTRA_BONJOUR_PEER = "extraBonjourPeer";
+
+ public static final String ACTION_STATUS = "BonjourStatus";
+ public static final String EXTRA_STATUS = "BonjourStatusExtra";
+ public static final int STATUS_STARTING = 0;
+ public static final int STATUS_STARTED = 1;
+ public static final int STATUS_STOPPING = 2;
+ public static final int STATUS_STOPPED = 3;
+ public static final int STATUS_VISIBLE = 4;
+ public static final int STATUS_NOT_VISIBLE = 5;
+ public static final int STATUS_ERROR = 0xffff;
+
+ public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
+ public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
+
+ private static final int STOP = 5709;
+ private static final int VISIBLE = 4151873;
+ private static final int NOT_VISIBLE = 144151873;
+
+ private static WeakReference context;
+ private static Handler handler;
+ private static volatile HandlerThread handlerThread;
+ private static ServiceInfo pairService;
+ private static JmDNS jmdns;
+ private static WifiManager.MulticastLock multicastLock;
+
+ public static boolean isAlive() {
+ return handlerThread != null && handlerThread.isAlive();
+ }
+
+ /**
+ * Stops the Bonjour/mDNS, triggering a status broadcast via {@link #ACTION_STATUS}.
+ * {@link #STATUS_STOPPED} can be broadcast multiple times for the same session,
+ * so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
+ */
+ public static void stop(Context context) {
+ BonjourManager.context = new WeakReference<>(context);
+ if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
+ sendBroadcast(STATUS_STOPPED, null);
+ return;
+ }
+ sendBroadcast(STATUS_STOPPING, null);
+ handler.sendEmptyMessage(STOP);
+ }
+
+ public static void setVisible(Context context, boolean visible) {
+ BonjourManager.context = new WeakReference<>(context);
+ if (handler == null || handlerThread == null || !handlerThread.isAlive()) {
+ Log.e(TAG, "handlerThread is stopped, not changing visibility!");
+ return;
+ }
+ if (visible) {
+ handler.sendEmptyMessage(VISIBLE);
+ } else {
+ handler.sendEmptyMessage(NOT_VISIBLE);
+ }
+ }
+
+ /**
+ * Starts the service, triggering a status broadcast via {@link #ACTION_STATUS}.
+ * {@link #STATUS_STARTED} can be broadcast multiple times for the same session,
+ * so make sure {@link android.content.BroadcastReceiver}s handle duplicates.
+ */
+ public static void start(Context context) {
+ start(context,
+ Preferences.get().getLocalRepoName(),
+ Preferences.get().isLocalRepoHttpsEnabled(),
+ httpServiceListener, httpsServiceListener);
+ }
+
+ /**
+ * Testable version, not for regular use.
+ *
+ * @see #start(Context)
+ */
+ static void start(final Context context,
+ final String localRepoName, final boolean useHttps,
+ final ServiceListener httpServiceListener, final ServiceListener httpsServiceListener) {
+ BonjourManager.context = new WeakReference<>(context);
+ if (handlerThread != null && handlerThread.isAlive()) {
+ sendBroadcast(STATUS_STARTED, null);
+ return;
+ }
+ sendBroadcast(STATUS_STARTING, null);
+
+ final WifiManager wifiManager = (WifiManager) context.getApplicationContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ handlerThread = new HandlerThread("BonjourManager", Process.THREAD_PRIORITY_LESS_FAVORABLE) {
+ @Override
+ protected void onLooperPrepared() {
+ try {
+ InetAddress address = InetAddress.getByName(FDroidApp.ipAddressString);
+ jmdns = JmDNS.create(address);
+ jmdns.addServiceListener(HTTP_SERVICE_TYPE, httpServiceListener);
+ jmdns.addServiceListener(HTTPS_SERVICE_TYPE, httpsServiceListener);
+
+ multicastLock = wifiManager.createMulticastLock(context.getPackageName());
+ multicastLock.setReferenceCounted(false);
+ multicastLock.acquire();
+
+ sendBroadcast(STATUS_STARTED, null);
+ } catch (IOException e) {
+ if (handler != null) {
+ handler.removeMessages(VISIBLE);
+ handler.sendMessageAtFrontOfQueue(handler.obtainMessage(STOP));
+ }
+ Log.e(TAG, "Error while registering jmdns service", e);
+ sendBroadcast(STATUS_ERROR, e.getLocalizedMessage());
+ }
+ }
+ };
+ handlerThread.start();
+ handler = new Handler(handlerThread.getLooper()) {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case VISIBLE:
+ handleVisible(localRepoName, useHttps);
+ break;
+ case NOT_VISIBLE:
+ handleNotVisible();
+ break;
+ case STOP:
+ handleStop();
+ break;
+ }
+ }
+
+ private void handleVisible(String localRepoName, boolean useHttps) {
+ HashMap values = new HashMap<>();
+ values.put(BonjourPeer.PATH, "/fdroid/repo");
+ values.put(BonjourPeer.NAME, localRepoName);
+ values.put(BonjourPeer.FINGERPRINT, FDroidApp.repo.fingerprint);
+ String type;
+ if (useHttps) {
+ values.put(BonjourPeer.TYPE, "fdroidrepos");
+ type = HTTPS_SERVICE_TYPE;
+ } else {
+ values.put(BonjourPeer.TYPE, "fdroidrepo");
+ type = HTTP_SERVICE_TYPE;
+ }
+ ServiceInfo newPairService = ServiceInfo.create(type, localRepoName, FDroidApp.port, 0, 0, values);
+ if (!newPairService.equals(pairService)) try {
+ if (pairService != null) {
+ jmdns.unregisterService(pairService);
+ }
+ jmdns.registerService(newPairService);
+ pairService = newPairService;
+ } catch (IOException e) {
+ e.printStackTrace();
+ sendBroadcast(STATUS_ERROR, e.getLocalizedMessage());
+ return;
+ }
+ sendBroadcast(STATUS_VISIBLE, null);
+ }
+
+ private void handleNotVisible() {
+ if (pairService != null) {
+ jmdns.unregisterService(pairService);
+ pairService = null;
+ }
+ sendBroadcast(STATUS_NOT_VISIBLE, null);
+ }
+
+ private void handleStop() {
+ if (multicastLock != null) {
+ multicastLock.release();
+ }
+ if (jmdns != null) {
+ jmdns.unregisterAllServices();
+ Utils.closeQuietly(jmdns);
+ pairService = null;
+ jmdns = null;
+ }
+ handlerThread.quit();
+ handlerThread = null;
+ sendBroadcast(STATUS_STOPPED, null);
+ }
+
+ };
+ }
+
+ public static void restart(Context context) {
+ restart(context,
+ Preferences.get().getLocalRepoName(),
+ Preferences.get().isLocalRepoHttpsEnabled(),
+ httpServiceListener, httpsServiceListener);
+ }
+
+ /**
+ * Testable version, not for regular use.
+ *
+ * @see #restart(Context)
+ */
+ static void restart(final Context context,
+ final String localRepoName, final boolean useHttps,
+ final ServiceListener httpServiceListener, final ServiceListener httpsServiceListener) {
+ stop(context);
+ try {
+ handlerThread.join(10000);
+ } catch (InterruptedException | NullPointerException e) {
+ // ignored
+ }
+ start(context, localRepoName, useHttps, httpServiceListener, httpsServiceListener);
+ }
+
+ private static void sendBroadcast(String action, ServiceInfo serviceInfo) {
+ BonjourPeer bonjourPeer = BonjourPeer.getInstance(serviceInfo);
+ if (bonjourPeer == null) {
+ Utils.debugLog(TAG, "IGNORING: " + serviceInfo);
+ return;
+ }
+ Intent intent = new Intent(action);
+ intent.putExtra(EXTRA_BONJOUR_PEER, bonjourPeer);
+ LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
+ }
+
+ private static void sendBroadcast(int status, String message) {
+
+ Intent intent = new Intent(ACTION_STATUS);
+ intent.putExtra(EXTRA_STATUS, status);
+ if (!TextUtils.isEmpty(message)) {
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ }
+ LocalBroadcastManager.getInstance(context.get()).sendBroadcast(intent);
+ }
+
+ private static final ServiceListener httpServiceListener = new SwapServiceListener();
+ private static final ServiceListener httpsServiceListener = new SwapServiceListener();
+
+ private static class SwapServiceListener implements ServiceListener {
+ @Override
+ public void serviceAdded(ServiceEvent serviceEvent) {
+ // ignored, we only need resolved info
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent serviceEvent) {
+ sendBroadcast(ACTION_REMOVED, serviceEvent.getInfo());
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent serviceEvent) {
+ sendBroadcast(ACTION_FOUND, serviceEvent.getInfo());
+ }
+ }
+}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java
deleted file mode 100644
index f6452c1eb..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.fdroid.fdroid.localrepo;
-
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import org.apache.commons.io.FileUtils;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.data.App;
-
-import java.io.File;
-import java.io.IOException;
-import java.security.cert.CertificateEncodingException;
-
-/**
- * An {@link IntentService} subclass for generating cached info about the installed APKs
- * which are available for swapping. It does not cache system apps, since those are
- * rarely swapped. This is meant to start running when {@link SwapService} starts.
- *
- * This could probably be replaced by {@link org.fdroid.fdroid.data.InstalledAppProvider}
- * if that contained all of the info to generate complete {@link App} and
- * {@link org.fdroid.fdroid.data.Apk} instances.
- */
-public class CacheSwapAppsService extends IntentService {
- private static final String TAG = "CacheSwapAppsService";
-
- private static final String ACTION_PARSE_APP = "org.fdroid.fdroid.localrepo.action.PARSE_APP";
-
- public CacheSwapAppsService() {
- super("CacheSwapAppsService");
- }
-
- /**
- * Parse the locally installed APK for {@code packageName} and save its XML
- * to the APK XML cache.
- */
- private static void parseApp(Context context, String packageName) {
- Intent intent = new Intent();
- intent.setData(Utils.getPackageUri(packageName));
- intent.setClass(context, CacheSwapAppsService.class);
- intent.setAction(ACTION_PARSE_APP);
- context.startService(intent);
- }
-
- /**
- * Parse all of the locally installed APKs into a memory cache, starting
- * with the currently selected apps. APKs that are already parsed in the
- * {@code index.jar} file will be read from that file.
- */
- public static void startCaching(Context context) {
- File indexJarFile = LocalRepoManager.get(context).getIndexJar();
- PackageManager pm = context.getPackageManager();
- for (ApplicationInfo applicationInfo : pm.getInstalledApplications(0)) {
- if (applicationInfo.publicSourceDir.startsWith(FDroidApp.SYSTEM_DIR_NAME)) {
- continue;
- }
- if (!indexJarFile.exists()
- || FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) {
- parseApp(context, applicationInfo.packageName);
- }
- }
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
- if (intent == null || !ACTION_PARSE_APP.equals(intent.getAction())) {
- Utils.debugLog(TAG, "received bad Intent: " + intent);
- return;
- }
-
- try {
- PackageManager pm = getPackageManager();
- String packageName = intent.getData().getSchemeSpecificPart();
- App app = App.getInstance(this, pm, packageName);
- if (app != null) {
- SwapService.putAppInCache(packageName, app);
- }
- } catch (CertificateEncodingException | IOException | PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- }
-}
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/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
index 1f818c7fb..c495ebd2e 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java
@@ -21,6 +21,8 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.InstalledApp;
+import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.SanitizedFile;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@@ -41,10 +43,10 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
@@ -69,7 +71,7 @@ public final class LocalRepoManager {
"swap-tick-not-done.png",
};
- private final Map apps = new HashMap<>();
+ private final Map apps = new ConcurrentHashMap<>();
private final SanitizedFile xmlIndexJar;
private final SanitizedFile xmlIndexJarUnsigned;
@@ -246,6 +248,10 @@ public final class LocalRepoManager {
return xmlIndexJar;
}
+ public File getWebRoot() {
+ return webRoot;
+ }
+
public void deleteRepo() {
deleteContents(repoDir);
}
@@ -270,12 +276,10 @@ public final class LocalRepoManager {
}
public void addApp(Context context, String packageName) {
- App app;
+ App app = null;
try {
- app = SwapService.getAppFromCache(packageName);
- if (app == null) {
- app = App.getInstance(context.getApplicationContext(), pm, packageName);
- }
+ InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName);
+ app = App.getInstance(context, pm, installedApp, packageName);
if (app == null || !app.isValid()) {
return;
}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java
new file mode 100644
index 000000000..42551bb78
--- /dev/null
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/LocalRepoService.java
@@ -0,0 +1,146 @@
+package org.fdroid.fdroid.localrepo;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.support.v4.content.LocalBroadcastManager;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Handles setting up and generating the local repo used to swap apps, including
+ * the {@code index.jar}, the symlinks to the shared APKs, etc.
+ *
+ * The work is done in a {@link Thread} so that new incoming {@code Intents}
+ * are not blocked by processing. A new {@code Intent} immediately nullifies
+ * the current state because it means the user has chosen a different set of
+ * apps. That is also enforced here since new {@code Intent}s with the same
+ * {@link Set} of apps as the current one are ignored. Having the
+ * {@code Thread} also makes it easy to kill work that is in progress.
+ */
+public class LocalRepoService extends IntentService {
+ public static final String TAG = "LocalRepoService";
+
+ public static final String ACTION_CREATE = "org.fdroid.fdroid.localrepo.action.CREATE";
+ public static final String EXTRA_PACKAGE_NAMES = "org.fdroid.fdroid.localrepo.extra.PACKAGE_NAMES";
+
+ public static final String ACTION_STATUS = "localRepoStatusAction";
+ public static final String EXTRA_STATUS = "localRepoStatusExtra";
+ public static final int STATUS_STARTED = 0;
+ public static final int STATUS_PROGRESS = 1;
+ public static final int STATUS_ERROR = 2;
+
+ private String[] currentlyProcessedApps = new String[0];
+
+ private GenerateLocalRepoThread thread;
+
+ public LocalRepoService() {
+ super("LocalRepoService");
+ }
+
+ /**
+ * Creates a skeleton swap repo with only F-Droid itself in it
+ */
+ public static void create(Context context) {
+ create(context, Collections.singleton(context.getPackageName()));
+ }
+
+ /**
+ * Sets up the local repo with the included {@code packageNames}
+ */
+ public static void create(Context context, Set packageNames) {
+ Intent intent = new Intent(context, LocalRepoService.class);
+ intent.setAction(ACTION_CREATE);
+ intent.putExtra(EXTRA_PACKAGE_NAMES, packageNames.toArray(new String[0]));
+ context.startService(intent);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
+ String[] packageNames = intent.getStringArrayExtra(EXTRA_PACKAGE_NAMES);
+ if (packageNames == null || packageNames.length == 0) {
+ Utils.debugLog(TAG, "no packageNames found, quiting");
+ return;
+ }
+ Arrays.sort(packageNames);
+
+ if (Arrays.equals(currentlyProcessedApps, packageNames)) {
+ Utils.debugLog(TAG, "packageNames list unchanged, quiting");
+ return;
+ }
+ currentlyProcessedApps = packageNames;
+
+ if (thread != null) {
+ thread.interrupt();
+ }
+ thread = new GenerateLocalRepoThread();
+ thread.start();
+ }
+
+ private class GenerateLocalRepoThread extends Thread {
+ private static final String TAG = "GenerateLocalRepoThread";
+
+ @Override
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+ runProcess(LocalRepoService.this, currentlyProcessedApps);
+ }
+ }
+
+ public static void runProcess(Context context, String[] selectedApps) {
+ try {
+ final LocalRepoManager lrm = LocalRepoManager.get(context);
+ broadcast(context, STATUS_PROGRESS, R.string.deleting_repo);
+ lrm.deleteRepo();
+ for (String app : selectedApps) {
+ broadcast(context, STATUS_PROGRESS, context.getString(R.string.adding_apks_format, app));
+ lrm.addApp(context, app);
+ }
+ String urlString = Utils.getSharingUri(FDroidApp.repo).toString();
+ lrm.writeIndexPage(urlString);
+ broadcast(context, STATUS_PROGRESS, R.string.writing_index_jar);
+ lrm.writeIndexJar();
+ broadcast(context, STATUS_PROGRESS, R.string.linking_apks);
+ lrm.copyApksToRepo();
+ broadcast(context, STATUS_PROGRESS, R.string.copying_icons);
+ // run the icon copy without progress, its not a blocker
+ new Thread() {
+ @Override
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
+ lrm.copyIconsToRepo();
+ }
+ }.start();
+
+ broadcast(context, STATUS_STARTED, null);
+ } catch (IOException | XmlPullParserException | LocalRepoKeyStore.InitException e) {
+ broadcast(context, STATUS_ERROR, e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Translate Android style broadcast {@link Intent}s to {@code PrepareSwapRepo}
+ */
+ static void broadcast(Context context, int status, String message) {
+ Intent intent = new Intent(ACTION_STATUS);
+ intent.putExtra(EXTRA_STATUS, status);
+ if (message != null) {
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ }
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+
+ static void broadcast(Context context, int status, int resId) {
+ broadcast(context, status, context.getString(resId));
+ }
+}
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 332cea135..1055ffd1c 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapService.java
@@ -14,8 +14,8 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.IBinder;
-import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
@@ -27,21 +27,13 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.localrepo.peers.Peer;
-import org.fdroid.fdroid.localrepo.peers.PeerFinder;
-import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
-import org.fdroid.fdroid.localrepo.type.SwapType;
-import org.fdroid.fdroid.localrepo.type.WifiSwap;
+import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
-import rx.Observable;
-import rx.Subscription;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
import java.io.IOException;
import java.io.OutputStream;
@@ -53,15 +45,12 @@ import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
-import java.util.concurrent.ConcurrentHashMap;
/**
* Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps.
*/
-@SuppressWarnings("LineLength")
public class SwapService extends Service {
-
private static final String TAG = "SwapService";
private static final String SHARED_PREFERENCES = "swap-state";
@@ -69,113 +58,58 @@ public class SwapService extends Service {
private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled";
private static final String KEY_WIFI_ENABLED = "wifiEnabled";
private static final String KEY_BLUETOOTH_ENABLED_BEFORE_SWAP = "bluetoothEnabledBeforeSwap";
+ private static final String KEY_BLUETOOTH_NAME_BEFORE_SWAP = "bluetoothNameBeforeSwap";
private static final String KEY_WIFI_ENABLED_BEFORE_SWAP = "wifiEnabledBeforeSwap";
@NonNull
private final Set appsToSwap = new HashSet<>();
+ private final Set activePeers = new HashSet<>();
- /**
- * A cache of parsed APKs from the file system.
- */
- private static final ConcurrentHashMap INSTALLED_APPS = new ConcurrentHashMap<>();
-
+ private static LocalBroadcastManager localBroadcastManager;
private static SharedPreferences swapPreferences;
private static BluetoothAdapter bluetoothAdapter;
private static WifiManager wifiManager;
+ private static Timer pollConnectedSwapRepoTimer;
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, SwapService.class);
+ if (Build.VERSION.SDK_INT < 26) {
+ context.startService(intent);
+ } else {
+ context.startForegroundService(intent);
+ }
+ }
public static void stop(Context context) {
Intent intent = new Intent(context, SwapService.class);
context.stopService(intent);
}
- static App getAppFromCache(String packageName) {
- return INSTALLED_APPS.get(packageName);
- }
-
- static void putAppInCache(String packageName, @NonNull App app) {
- INSTALLED_APPS.put(packageName, app);
- }
-
- // ==========================================================
- // Search for peers to swap
- // ==========================================================
-
- private Observable peerFinder;
-
- /**
- * Call {@link Observable#subscribe()} on this in order to be notified of peers
- * which are found. Call {@link Subscription#unsubscribe()} on the resulting
- * subscription when finished and you no longer want to scan for peers.
- *
- * The returned object will scan for peers on a background thread, and emit
- * found peers on the mian thread.
- *
- * Invoking this in multiple places will return the same, cached, peer finder.
- * That is, if in the past it already found some peers, then you subscribe
- * to it in the future, the future subscriber will still receive the peers
- * that were found previously.
- * TODO: What about removing peers that no longer are present?
- */
- public Observable scanForPeers() {
- Utils.debugLog(TAG, "Scanning for nearby devices to swap with...");
- if (peerFinder == null) {
- peerFinder = PeerFinder.createObservable(getApplicationContext())
- .subscribeOn(Schedulers.newThread())
- .observeOn(AndroidSchedulers.mainThread())
- .distinct();
- }
- return peerFinder;
- }
-
- public static final int STEP_INTRO = 1;
-
- @LayoutRes
- private int currentView = STEP_INTRO;
-
- /**
- * Current screen that the swap process is up to.
- */
- @LayoutRes
- public int getCurrentView() {
- return currentView;
- }
-
- public void setCurrentView(@LayoutRes int currentView) {
- this.currentView = currentView;
- }
-
@NonNull
public Set getAppsToSwap() {
return appsToSwap;
}
- public void refreshSwap() {
- if (peer != null) {
- connectTo(peer, false);
- }
+ @NonNull
+ public Set getActivePeers() {
+ return activePeers;
}
public void connectToPeer() {
if (getPeer() == null) {
throw new IllegalStateException("Cannot connect to peer, no peer has been selected.");
}
- connectTo(getPeer(), getPeer().shouldPromptForSwapBack());
+ connectTo(getPeer());
+ if (LocalHTTPDManager.isAlive() && 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());
}
@@ -205,8 +139,14 @@ public class SwapService extends Service {
"POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode);
} catch (IOException e) {
Log.e(TAG, "Error while asking server to swap with us", e);
+ Intent intent = new Intent(Downloader.ACTION_INTERRUPTED);
+ intent.setData(Uri.parse(repo.address));
+ intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage());
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
} finally {
- conn.disconnect();
+ if (conn != null) {
+ conn.disconnect();
+ }
}
return null;
}
@@ -260,6 +200,14 @@ public class SwapService extends Service {
this.peer = peer;
}
+ public void addCurrentPeerToActive() {
+ activePeers.add(peer);
+ }
+
+ public void removeCurrentPeerFromActive() {
+ activePeers.remove(peer);
+ }
+
public boolean isConnectingWithPeer() {
return peer != null;
}
@@ -354,6 +302,14 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, visible).apply();
}
+ public static String getBluetoothNameBeforeSwap() {
+ return swapPreferences.getString(SwapService.KEY_BLUETOOTH_NAME_BEFORE_SWAP, null);
+ }
+
+ public static void putBluetoothNameBeforeSwap(String name) {
+ swapPreferences.edit().putString(SwapService.KEY_BLUETOOTH_NAME_BEFORE_SWAP, name).apply();
+ }
+
public static boolean wasWifiEnabledBeforeSwap() {
return swapPreferences.getBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, false);
}
@@ -362,60 +318,9 @@ public class SwapService extends Service {
swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, visible).apply();
}
- /**
- * Handles checking if the {@link SwapService} is running, and only restarts it if it was running.
- */
- public void stopWifiIfEnabled(final boolean restartAfterStopping) {
- if (wifiSwap.isConnected()) {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- Utils.debugLog(TAG, "Stopping the currently running WiFi swap service (on background thread)");
- wifiSwap.stop();
-
- if (restartAfterStopping) {
- Utils.debugLog(TAG, "Restarting WiFi swap service after stopping (still on background thread)");
- wifiSwap.start();
- }
- return null;
- }
- }.execute();
- }
- }
-
- public boolean isEnabled() {
- return bluetoothSwap.isConnected() || wifiSwap.isConnected();
- }
-
- // ==========================================
- // Interacting with Bluetooth adapter
- // ==========================================
-
- public boolean isBluetoothDiscoverable() {
- return bluetoothSwap.isDiscoverable();
- }
-
- public boolean isBonjourDiscoverable() {
- return wifiSwap.isConnected() && wifiSwap.getBonjour().isConnected();
- }
-
- // ===============================================================
- // Old SwapService stuff being merged into that.
- // ===============================================================
-
- public static final String BONJOUR_STATE_CHANGE = "org.fdroid.fdroid.BONJOUR_STATE_CHANGE";
- public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE";
- public static final String WIFI_STATE_CHANGE = "org.fdroid.fdroid.WIFI_STATE_CHANGE";
- public static final String EXTRA_STARTING = "STARTING";
- public static final String EXTRA_STARTED = "STARTED";
- public static final String EXTRA_STOPPING = "STOPPING";
- public static final String EXTRA_STOPPED = "STOPPED";
-
private static final int NOTIFICATION = 1;
private final Binder binder = new Binder();
- private SwapType bluetoothSwap;
- private WifiSwap wifiSwap;
private static final int TIMEOUT = 15 * 60 * 1000; // 15 mins
@@ -425,14 +330,6 @@ public class SwapService extends Service {
@Nullable
private Timer timer;
- public SwapType getBluetoothSwap() {
- return bluetoothSwap;
- }
-
- public WifiSwap getWifiSwap() {
- return wifiSwap;
- }
-
public class Binder extends android.os.Binder {
public SwapService getService() {
return SwapService.this;
@@ -441,19 +338,20 @@ public class SwapService extends Service {
public void onCreate() {
super.onCreate();
-
- Utils.debugLog(TAG, "Creating swap service.");
startForeground(NOTIFICATION, createNotification());
-
- deleteAllSwapRepos();
-
- CacheSwapAppsService.startCaching(this);
-
+ localBroadcastManager = LocalBroadcastManager.getInstance(this);
swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE);
+ LocalHTTPDManager.start(this);
+
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
SwapService.putBluetoothEnabledBeforeSwap(bluetoothAdapter.isEnabled());
+ if (bluetoothAdapter.isEnabled()) {
+ BluetoothManager.start(this);
+ }
+ registerReceiver(bluetoothScanModeChanged,
+ new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
}
wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
@@ -462,32 +360,30 @@ public class SwapService extends Service {
}
appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, "")));
- bluetoothSwap = BluetoothSwap.create(this);
- wifiSwap = new WifiSwap(this, wifiManager);
Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener);
- LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
- new IntentFilter(WifiStateChangeService.BROADCAST));
+ localBroadcastManager.registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST));
+ localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS));
+ localBroadcastManager.registerReceiver(bluetoothPeerFound, new IntentFilter(BluetoothManager.ACTION_FOUND));
+ localBroadcastManager.registerReceiver(bonjourPeerFound, new IntentFilter(BonjourManager.ACTION_FOUND));
+ localBroadcastManager.registerReceiver(bonjourPeerRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED));
+ localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS));
- if (getBluetoothVisibleUserPreference()) {
- Utils.debugLog(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically.");
- bluetoothSwap.startInBackground(); // TODO replace with Intent to SwapService
- } else {
- Utils.debugLog(TAG, "Bluetooth was NOT enabled last time user swapped, starting not visible.");
- }
-
- if (getWifiVisibleUserPreference()) {
- Utils.debugLog(TAG, "Previously the user enabled WiFi swap, so enabling again automatically.");
- wifiSwap.startInBackground(); // TODO replace with Intent to SwapService
- } else {
- Utils.debugLog(TAG, "WiFi was NOT enabled last time user swapped, starting not visible.");
- }
+ BonjourManager.start(this);
+ BonjourManager.setVisible(this, getWifiVisibleUserPreference());
}
+ /**
+ * This is for setting things up for when the {@code SwapService} was
+ * started by the user clicking on the initial start button. The things
+ * that must be run always on start-up go in {@link #onCreate()}.
+ */
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- return START_STICKY;
+ deleteAllSwapRepos();
+ startActivity(new Intent(this, SwapWorkflowActivity.class));
+ return START_NOT_STICKY;
}
@Override
@@ -501,18 +397,23 @@ public class SwapService extends Service {
public void onDestroy() {
Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners.");
Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener);
- LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
+ localBroadcastManager.unregisterReceiver(onWifiChange);
+ localBroadcastManager.unregisterReceiver(bluetoothStatus);
+ localBroadcastManager.unregisterReceiver(bluetoothPeerFound);
+ localBroadcastManager.unregisterReceiver(bonjourPeerFound);
+ localBroadcastManager.unregisterReceiver(bonjourPeerRemoved);
- if (bluetoothAdapter != null && !wasBluetoothEnabledBeforeSwap()) {
- bluetoothAdapter.disable();
- }
+ unregisterReceiver(bluetoothScanModeChanged);
+ BluetoothManager.stop(this);
+
+ BonjourManager.stop(this);
+ LocalHTTPDManager.stop(this);
if (wifiManager != null && !wasWifiEnabledBeforeSwap()) {
wifiManager.setWifiEnabled(false);
}
- //TODO getBluetoothSwap().stopInBackground();
- getWifiSwap().stopInBackground();
+ stopPollingConnectedSwapRepo();
if (timer != null) {
timer.cancel();
@@ -554,40 +455,151 @@ public class SwapService extends Service {
}
}
- private void initTimer() {
- // TODO replace by Android scheduler
+ private void startPollingConnectedSwapRepo() {
+ stopPollingConnectedSwapRepo();
+ pollConnectedSwapRepoTimer = new Timer("pollConnectedSwapRepoTimer", true);
+ TimerTask timerTask = new TimerTask() {
+ @Override
+ public void run() {
+ if (peer != null) {
+ connectTo(peer);
+ }
+ }
+ };
+ pollConnectedSwapRepoTimer.schedule(timerTask, 5000);
+ }
+
+ public void stopPollingConnectedSwapRepo() {
+ if (pollConnectedSwapRepoTimer != null) {
+ pollConnectedSwapRepoTimer.cancel();
+ pollConnectedSwapRepoTimer = null;
+ }
+ }
+
+ /**
+ * Sets or resets the idel timer for {@link #TIMEOUT}ms, once the timer
+ * expires, this service and all things that rely on it will be stopped.
+ */
+ public void initTimer() {
if (timer != null) {
Utils.debugLog(TAG, "Cancelling existing timeout timer so timeout can be reset.");
timer.cancel();
}
Utils.debugLog(TAG, "Initializing swap timeout to " + TIMEOUT + "ms minutes");
- timer = new Timer();
+ timer = new Timer(TAG, true);
timer.schedule(new TimerTask() {
@Override
public void run() {
Utils.debugLog(TAG, "Disabling swap because " + TIMEOUT + "ms passed.");
+ String msg = getString(R.string.swap_toast_closing_nearby_after_timeout);
+ Utils.showToastFromService(SwapService.this, msg, android.widget.Toast.LENGTH_LONG);
stop(SwapService.this);
}
}, TIMEOUT);
}
- @SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
+ private void restartWiFiServices() {
+ boolean hasIp = FDroidApp.ipAddressString != null;
+ if (hasIp) {
+ LocalHTTPDManager.restart(this);
+ BonjourManager.restart(this);
+ BonjourManager.setVisible(this, getWifiVisibleUserPreference());
+ } else {
+ BonjourManager.stop(this);
+ LocalHTTPDManager.stop(this);
+ }
+ }
+
private final Preferences.ChangeListener httpsEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
- Log.i(TAG, "Swap over HTTPS preference changed.");
- stopWifiIfEnabled(true);
+ restartWiFiServices();
}
};
- @SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
- boolean hasIp = FDroidApp.ipAddressString != null;
- stopWifiIfEnabled(hasIp);
+ restartWiFiServices();
}
};
-}
+ private final BroadcastReceiver bluetoothStatus = new SwapStateChangeReceiver();
+ private final BroadcastReceiver localRepoStatus = new SwapStateChangeReceiver();
+
+ /**
+ * When swapping is setup, then start the index polling.
+ */
+ private class SwapStateChangeReceiver extends BroadcastReceiver {
+ private final BroadcastReceiver pollForUpdatesReceiver = new PollForUpdatesReceiver();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int bluetoothStatus = intent.getIntExtra(BluetoothManager.ACTION_STATUS, -1);
+ int wifiStatus = intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1);
+ if (bluetoothStatus == BluetoothManager.STATUS_STARTED
+ || wifiStatus == LocalRepoService.STATUS_STARTED) {
+ localBroadcastManager.registerReceiver(pollForUpdatesReceiver,
+ new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
+ } else {
+ localBroadcastManager.unregisterReceiver(pollForUpdatesReceiver);
+ }
+ }
+ }
+
+ /**
+ * Reschedule an index update if the last one was successful.
+ */
+ private class PollForUpdatesReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1)) {
+ case UpdateService.STATUS_COMPLETE_AND_SAME:
+ case UpdateService.STATUS_COMPLETE_WITH_CHANGES:
+ startPollingConnectedSwapRepo();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle events if the user or system changes the Bluetooth setup outside of F-Droid.
+ */
+ private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
+ case BluetoothAdapter.SCAN_MODE_NONE:
+ BluetoothManager.stop(SwapService.this);
+ break;
+
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+ BluetoothManager.start(SwapService.this);
+ break;
+ }
+ }
+ };
+
+ private final BroadcastReceiver bluetoothPeerFound = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ activePeers.add((Peer) intent.getParcelableExtra(BluetoothManager.EXTRA_PEER));
+ }
+ };
+
+ private final BroadcastReceiver bonjourPeerFound = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ activePeers.add((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
+ }
+ };
+
+ private final BroadcastReceiver bonjourPeerRemoved = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ activePeers.remove((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapView.java b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapView.java
index 9404ad14f..668827827 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/SwapView.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/SwapView.java
@@ -21,7 +21,7 @@ public class SwapView extends RelativeLayout {
public final int toolbarColor;
public final String toolbarTitle;
- private int layoutResId;
+ private int layoutResId = -1;
protected String currentFilterString;
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java
deleted file mode 100644
index 8c024e3a5..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothFinder.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.fdroid.fdroid.localrepo.peers;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothClass;
-import android.bluetooth.BluetoothDevice;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-import org.fdroid.fdroid.Utils;
-
-import rx.Observable;
-import rx.Subscriber;
-import rx.functions.Action0;
-import rx.subscriptions.Subscriptions;
-
-@SuppressWarnings("LineLength")
-final class BluetoothFinder extends PeerFinder {
-
- public static Observable createBluetoothObservable(final Context context) {
- return Observable.create(new Observable.OnSubscribe() {
- @Override
- public void call(Subscriber super Peer> subscriber) {
- final BluetoothFinder finder = new BluetoothFinder(context, subscriber);
-
- subscriber.add(Subscriptions.create(new Action0() {
- @Override
- public void call() {
- finder.cancel();
- }
- }));
-
- finder.scan();
- }
- });
- }
-
- private static final String TAG = "BluetoothFinder";
-
- private final BluetoothAdapter adapter;
-
- private BluetoothFinder(Context context, Subscriber super Peer> subscriber) {
- super(context, subscriber);
- adapter = BluetoothAdapter.getDefaultAdapter();
- }
-
- private BroadcastReceiver deviceFoundReceiver;
- private BroadcastReceiver scanCompleteReceiver;
-
- private void scan() {
-
- if (adapter == null) {
- Log.i(TAG, "Not scanning for bluetooth peers to swap with, couldn't find a bluetooth adapter on this device.");
- return;
- }
-
- isScanning = true;
-
- if (deviceFoundReceiver == null) {
- deviceFoundReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
- BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- onDeviceFound(device);
- }
- }
- };
- context.registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
- }
-
- if (scanCompleteReceiver == null) {
- scanCompleteReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (isScanning) {
- Utils.debugLog(TAG, "Scan complete, but we haven't been asked to stop scanning yet, so will restart scan.");
- startDiscovery();
- }
- }
- };
-
- // TODO: Unregister this receiver at the appropriate time.
- context.registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
- }
-
- startDiscovery();
- }
-
- private void startDiscovery() {
-
- if (adapter.isDiscovering()) {
- // TODO: Can we reset the discovering timeout, so that it doesn't, e.g. time out in 3
- // seconds because we had already almost completed the previous scan? We could
- // cancelDiscovery(), but then it will probably prompt the user again.
- Utils.debugLog(TAG, "Requested bluetooth scan when already scanning, so will ignore request.");
- return;
- }
-
- if (!adapter.startDiscovery()) {
- Log.e(TAG, "Couldn't start bluetooth scanning.");
- }
-
- }
-
- private void cancel() {
- if (adapter != null) {
- Utils.debugLog(TAG, "Stopping bluetooth discovery.");
- adapter.cancelDiscovery();
- }
-
- isScanning = false;
- }
-
- private void onDeviceFound(BluetoothDevice device) {
- if (device != null && device.getName() != null &&
- (device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA ||
- device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA ||
- device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.PHONE_SMART)) {
- subscriber.onNext(new BluetoothPeer(device));
- }
- }
-
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
index 5cc75f8b8..42b9c1153 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
@@ -1,16 +1,34 @@
package org.fdroid.fdroid.localrepo.peers;
+import android.bluetooth.BluetoothClass.Device;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
-
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
public class BluetoothPeer implements Peer {
+ private static final String BLUETOOTH_NAME_TAG = "FDroid:";
+
private final BluetoothDevice device;
- public BluetoothPeer(BluetoothDevice device) {
+ /**
+ * Return a instance if the {@link BluetoothDevice} is a device that could
+ * host a swap repo.
+ */
+ @Nullable
+ public static BluetoothPeer getInstance(@Nullable BluetoothDevice device) {
+ if (device != null && device.getName() != null &&
+ (device.getBluetoothClass().getDeviceClass() == Device.COMPUTER_HANDHELD_PC_PDA
+ || device.getBluetoothClass().getDeviceClass() == Device.COMPUTER_PALM_SIZE_PC_PDA
+ || device.getBluetoothClass().getDeviceClass() == Device.PHONE_SMART)) {
+ return new BluetoothPeer(device);
+ }
+ return null;
+ }
+
+ private BluetoothPeer(BluetoothDevice device) {
this.device = device;
}
@@ -21,7 +39,7 @@ public class BluetoothPeer implements Peer {
@Override
public String getName() {
- return device.getName().replaceAll("^" + BluetoothSwap.BLUETOOTH_NAME_TAG, "");
+ return device.getName().replaceAll("^" + BLUETOOTH_NAME_TAG, "");
}
@Override
@@ -31,9 +49,8 @@ public class BluetoothPeer implements Peer {
@Override
public boolean equals(Object peer) {
- return peer != null
- && peer instanceof BluetoothPeer
- && ((BluetoothPeer) peer).device.getAddress().equals(device.getAddress());
+ return peer instanceof BluetoothPeer
+ && TextUtils.equals(((BluetoothPeer) peer).device.getAddress(), device.getAddress());
}
@Override
@@ -48,7 +65,7 @@ public class BluetoothPeer implements Peer {
/**
* Return the fingerprint of the signing key, or {@code null} if it is not set.
- *
+ *
* This is not yet stored for Bluetooth connections. Once a device is connected to a bluetooth
* socket, if we trust it enough to accept a fingerprint from it somehow, then we may as well
* trust it enough to receive an index from it that contains a fingerprint we can use.
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
deleted file mode 100644
index e1354019a..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package org.fdroid.fdroid.localrepo.peers;
-
-import android.content.Context;
-import android.net.wifi.WifiManager;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Utils;
-import rx.Observable;
-import rx.Subscriber;
-import rx.functions.Action0;
-import rx.subscriptions.Subscriptions;
-
-import javax.jmdns.JmDNS;
-import javax.jmdns.ServiceEvent;
-import javax.jmdns.ServiceInfo;
-import javax.jmdns.ServiceListener;
-import java.io.IOException;
-import java.net.InetAddress;
-
-@SuppressWarnings("LineLength")
-final class BonjourFinder extends PeerFinder implements ServiceListener {
-
- public static Observable createBonjourObservable(final Context context) {
- return Observable.create(new Observable.OnSubscribe() {
- @Override
- public void call(Subscriber super Peer> subscriber) {
- final BonjourFinder finder = new BonjourFinder(context, subscriber);
-
- subscriber.add(Subscriptions.create(new Action0() {
- @Override
- public void call() {
- finder.cancel();
- }
- }));
-
- finder.scan();
- }
- });
- }
-
- private static final String TAG = "BonjourFinder";
-
- private static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
- private static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
-
- private JmDNS jmdns;
- private WifiManager wifiManager;
- private WifiManager.MulticastLock multicastLock;
-
- private BonjourFinder(Context context, Subscriber super Peer> subscriber) {
- super(context, subscriber);
- }
-
- private void scan() {
-
- Utils.debugLog(TAG, "Requested Bonjour (mDNS) scan for peers.");
-
- if (wifiManager == null) {
- wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- multicastLock = wifiManager.createMulticastLock(context.getPackageName());
- multicastLock.setReferenceCounted(false);
- }
-
- if (isScanning) {
- Utils.debugLog(TAG, "Requested Bonjour scan, but already scanning. But we will still try to explicitly scan for services.");
- return;
- }
-
- isScanning = true;
- multicastLock.acquire();
-
- try {
- Utils.debugLog(TAG, "Searching for Bonjour (mDNS) clients...");
- jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
- } catch (IOException e) {
- subscriber.onError(e);
- return;
- }
-
- Utils.debugLog(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE);
- jmdns.addServiceListener(HTTP_SERVICE_TYPE, this);
- jmdns.addServiceListener(HTTPS_SERVICE_TYPE, this);
- listServices();
- }
-
- private void listServices() {
- Utils.debugLog(TAG, "Explicitly querying for services, in addition to waiting for notifications.");
- addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE));
- addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE));
- }
-
- @Override
- public void serviceRemoved(ServiceEvent event) {
- }
-
- @Override
- public void serviceAdded(final ServiceEvent event) {
- // TODO: Get clarification, but it looks like this is:
- // 1) Identifying that there is _a_ bonjour service available
- // 2) Adding it to the list to give some sort of feedback to the user
- // 3) Requesting more detailed info in an async manner
- // 4) If that is in fact an fdroid repo (after requesting info), then add it again
- // so that more detailed info can be shown to the user.
- //
- // If so, when is the old one removed?
- addFDroidService(event.getInfo());
-
- Utils.debugLog(TAG, "Found JmDNS service, now requesting further details of service");
- jmdns.requestServiceInfo(event.getType(), event.getName(), true);
- }
-
- @Override
- public void serviceResolved(ServiceEvent event) {
- addFDroidService(event.getInfo());
- }
-
- private void addFDroidServices(ServiceInfo[] services) {
- for (ServiceInfo info : services) {
- addFDroidService(info);
- }
- }
-
- /**
- * Broadcasts the fact that a Bonjour peer was found to swap with.
- * Checks that the service is an F-Droid service, and also that it is not the F-Droid service
- * for this device (by comparing its signing fingerprint to our signing fingerprint).
- */
- private void addFDroidService(ServiceInfo serviceInfo) {
- final String type = serviceInfo.getPropertyString("type");
- final String fingerprint = serviceInfo.getPropertyString("fingerprint");
- final boolean isFDroid = type != null && type.startsWith("fdroidrepo");
- final boolean isSelf = FDroidApp.repo != null && fingerprint != null && fingerprint.equalsIgnoreCase(FDroidApp.repo.fingerprint);
- if (isFDroid && !isSelf) {
- Utils.debugLog(TAG, "Found F-Droid swap Bonjour service:\n" + serviceInfo);
- subscriber.onNext(new BonjourPeer(serviceInfo));
- } else {
- if (isSelf) {
- Utils.debugLog(TAG, "Ignoring Bonjour service because it belongs to this device:\n" + serviceInfo);
- } else {
- Utils.debugLog(TAG, "Ignoring Bonjour service because it doesn't look like an F-Droid swap repo:\n" + serviceInfo);
- }
- }
- }
-
- private void cancel() {
- Utils.debugLog(TAG, "Cancelling BonjourFinder, releasing multicast lock, removing jmdns service listeners");
-
- if (multicastLock != null) {
- multicastLock.release();
- }
-
- isScanning = false;
-
- if (jmdns == null) {
- return;
- }
- jmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
- jmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
- jmdns = null;
-
- }
-
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
index 0552ebf67..b40809dfa 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
@@ -2,15 +2,39 @@ package org.fdroid.fdroid.localrepo.peers;
import android.net.Uri;
import android.os.Parcel;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import org.fdroid.fdroid.FDroidApp;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.FDroidServiceInfo;
public class BonjourPeer extends WifiPeer {
+ private static final String TAG = "BonjourPeer";
+
+ public static final String FINGERPRINT = "fingerprint";
+ public static final String NAME = "name";
+ public static final String PATH = "path";
+ public static final String TYPE = "type";
private final FDroidServiceInfo serviceInfo;
- public BonjourPeer(ServiceInfo serviceInfo) {
+ /**
+ * Return a instance if the {@link ServiceInfo} is fully resolved and does
+ * not represent this device, but something else on the network.
+ */
+ @Nullable
+ public static BonjourPeer getInstance(ServiceInfo serviceInfo) {
+ String type = serviceInfo.getPropertyString(TYPE);
+ String fingerprint = serviceInfo.getPropertyString(FINGERPRINT);
+ if (type == null || !type.startsWith("fdroidrepo")
+ || TextUtils.equals(FDroidApp.repo.fingerprint, fingerprint)) {
+ return null;
+ }
+ return new BonjourPeer(serviceInfo);
+ }
+
+ private BonjourPeer(ServiceInfo serviceInfo) {
this.serviceInfo = new FDroidServiceInfo(serviceInfo);
this.name = serviceInfo.getDomain();
this.uri = Uri.parse(this.serviceInfo.getRepoAddress());
@@ -27,15 +51,6 @@ public class BonjourPeer extends WifiPeer {
return serviceInfo.getName();
}
- @Override
- public boolean equals(Object peer) {
- if (peer != null && peer instanceof BonjourPeer) {
- BonjourPeer that = (BonjourPeer) peer;
- return this.getFingerprint().equals(that.getFingerprint());
- }
- return false;
- }
-
@Override
public int hashCode() {
String fingerprint = getFingerprint();
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/Peer.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/Peer.java
index c1481e297..14a45db29 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/Peer.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/Peer.java
@@ -3,11 +3,20 @@ package org.fdroid.fdroid.localrepo.peers;
import android.os.Parcelable;
import android.support.annotation.DrawableRes;
+/**
+ * TODO This model assumes that "peers" from Bluetooth, Bonjour, and WiFi are
+ * different things. They are not different repos though, they all point to
+ * the same repos. This should really be combined to be a single "RemoteRepo"
+ * class that represents a single device's local repo, and can have zero to
+ * many ways to connect to it (e.g. Bluetooth, WiFi, USB Thumb Drive, SD Card,
+ * WiFi Direct, etc).
+ */
public interface Peer extends Parcelable {
String getName();
- @DrawableRes int getIcon();
+ @DrawableRes
+ int getIcon();
boolean equals(Object peer);
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/PeerFinder.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/PeerFinder.java
deleted file mode 100644
index 6cbffe4bc..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/PeerFinder.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.fdroid.fdroid.localrepo.peers;
-
-import android.content.Context;
-
-import rx.Observable;
-import rx.Subscriber;
-import rx.schedulers.Schedulers;
-
-/**
- * Searches for other devices in the vicinity, using specific technologies.
- * Once found, emits a {@link Peer} to interested {@link Subscriber}s.
- */
-public abstract class PeerFinder {
-
- protected boolean isScanning;
- protected final Context context;
- protected final Subscriber super Peer> subscriber;
-
- protected PeerFinder(Context context, Subscriber super Peer> subscriber) {
- this.context = context;
- this.subscriber = subscriber;
- }
-
- public static Observable createObservable(final Context context) {
- return Observable.merge(
- BluetoothFinder.createBluetoothObservable(context).subscribeOn(Schedulers.newThread()),
- BonjourFinder.createBonjourObservable(context).subscribeOn(Schedulers.newThread())
- );
- }
-
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/WifiPeer.java b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/WifiPeer.java
index 161e2fbd0..03d913bab 100644
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/peers/WifiPeer.java
+++ b/app/src/full/java/org/fdroid/fdroid/localrepo/peers/WifiPeer.java
@@ -2,7 +2,7 @@ package org.fdroid.fdroid.localrepo.peers;
import android.net.Uri;
import android.os.Parcel;
-
+import android.text.TextUtils;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.NewRepoConfig;
@@ -26,6 +26,35 @@ public class WifiPeer implements Peer {
this.shouldPromptForSwapBack = shouldPromptForSwapBack;
}
+ /**
+ * Return if this instance points to the same device as that instance, even
+ * if some of the configuration details are not the same, like whether one
+ * instance supplies the fingerprint and the other does not, then use IP
+ * address and port number.
+ */
+ @Override
+ public boolean equals(Object peer) {
+ if (peer instanceof BluetoothPeer) {
+ return false;
+ }
+ String fingerprint = getFingerprint();
+ if (this instanceof BonjourPeer && peer instanceof BonjourPeer) {
+ BonjourPeer that = (BonjourPeer) peer;
+ return TextUtils.equals(this.getFingerprint(), that.getFingerprint());
+ } else {
+ WifiPeer that = (WifiPeer) peer;
+ if (!TextUtils.isEmpty(fingerprint) && TextUtils.equals(this.getFingerprint(), that.getFingerprint())) {
+ return true;
+ }
+ return TextUtils.equals(this.getRepoAddress(), that.getRepoAddress());
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return (uri.getHost() + uri.getPort()).hashCode();
+ }
+
@Override
public String getName() {
return name;
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java b/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java
deleted file mode 100644
index 7ab9735b1..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java
+++ /dev/null
@@ -1,186 +0,0 @@
-package org.fdroid.fdroid.localrepo.type;
-
-import android.bluetooth.BluetoothAdapter;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.SwapService;
-import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
-
-@SuppressWarnings("LineLength")
-public final class BluetoothSwap extends SwapType {
-
- private static final String TAG = "BluetoothSwap";
- public static final String BLUETOOTH_NAME_TAG = "FDroid:";
-
- private static BluetoothSwap mInstance;
-
- @NonNull
- private final BluetoothAdapter adapter;
- private boolean isDiscoverable;
-
- @Nullable
- private BluetoothServer server;
-
- private String deviceBluetoothName;
-
- public static SwapType create(@NonNull Context context) {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- if (adapter == null) {
- return new NoBluetoothType(context);
- }
- if (mInstance == null) {
- mInstance = new BluetoothSwap(context, adapter);
- }
-
- return mInstance;
- }
-
- private BluetoothSwap(@NonNull Context context, @NonNull BluetoothAdapter adapter) {
- super(context);
- this.adapter = adapter;
- }
-
- @Override
- public boolean isDiscoverable() {
- return isDiscoverable;
- }
-
- @Override
- public boolean isConnected() {
- return server != null && server.isRunning() && super.isConnected();
- }
-
- @Override
- public synchronized void start() {
- if (isConnected()) {
- Utils.debugLog(TAG, "already running, quitting start()");
- return;
- }
-
- BroadcastReceiver receiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
- case BluetoothAdapter.SCAN_MODE_NONE:
- setConnected(false);
- break;
-
- case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
- isDiscoverable = true;
- if (server != null && server.isRunning()) {
- setConnected(true);
- }
- break;
-
- // Only other is BluetoothAdapter.SCAN_MODE_CONNECTABLE. For now don't handle that.
- }
- }
- };
- context.registerReceiver(receiver, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
-
- /*
- if (server != null) {
- Utils.debugLog(TAG, "Attempting to start Bluetooth swap, but it appears to be running already. Will cancel it so it can be restarted.");
- server.close();
- server = null;
- }*/
-
- if (server == null) {
- server = new BluetoothServer(this, context.getFilesDir());
- }
-
- sendBroadcast(SwapService.EXTRA_STARTING);
-
- //store the original bluetoothname, and update this one to be unique
- deviceBluetoothName = adapter.getName();
-
- /*
- Utils.debugLog(TAG, "Prefixing Bluetooth adapter name with " + BLUETOOTH_NAME_TAG + " to make it identifiable as a swap device.");
- if (!deviceBluetoothName.startsWith(BLUETOOTH_NAME_TAG)) {
- adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName);
- }
-
- if (!adapter.getName().startsWith(BLUETOOTH_NAME_TAG)) {
- Log.e(TAG, "Couldn't change the name of the Bluetooth adapter, it will not get recognized by other swap clients.");
- // TODO: Should we bail here?
- }*/
-
- if (!adapter.isEnabled()) {
- Utils.debugLog(TAG, "Bluetooth adapter is disabled, attempting to enable.");
- if (!adapter.enable()) {
- Utils.debugLog(TAG, "Could not enable Bluetooth adapter, so bailing out of Bluetooth swap.");
- setConnected(false);
- return;
- }
- }
-
- if (adapter.isEnabled()) {
- setConnected(true);
- } else {
- Log.i(TAG, "Didn't start Bluetooth swapping server, because Bluetooth is disabled and couldn't be enabled.");
- setConnected(false);
- }
- }
-
- /**
- * Don't try to start BT in the background. you can only start/stop a BT server once, else new connections don't work.
- */
- @Override
- public void stopInBackground() {
- stop();
- }
-
- @Override
- public void stop() {
- if (server != null && server.isAlive()) {
- server.close();
- setConnected(false);
-
- /*
- if (receiver != null) {
- context.unregisterReceiver(receiver);
- receiver = null;
- }
- */
- } else {
- Log.i(TAG, "Attempting to stop Bluetooth swap, but it is not currently running.");
- }
- }
-
- protected void onStopped() {
- Utils.debugLog(TAG, "Resetting bluetooth device name to " + deviceBluetoothName + " after swapping.");
- adapter.setName(deviceBluetoothName);
- }
-
- @Override
- public String getBroadcastAction() {
- return SwapService.BLUETOOTH_STATE_CHANGE;
- }
-
- private static class NoBluetoothType extends SwapType {
-
- NoBluetoothType(@NonNull Context context) {
- super(context);
- }
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- protected String getBroadcastAction() {
- return null;
- }
-
- }
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BonjourBroadcast.java b/app/src/full/java/org/fdroid/fdroid/localrepo/type/BonjourBroadcast.java
deleted file mode 100644
index 8a8c467de..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/BonjourBroadcast.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package org.fdroid.fdroid.localrepo.type;
-
-import android.content.Context;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.SwapService;
-
-import javax.jmdns.JmDNS;
-import javax.jmdns.ServiceInfo;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.HashMap;
-
-/**
- * Sends a {@link SwapService#BONJOUR_STATE_CHANGE} broadcasts when starting, started or stopped.
- */
-public class BonjourBroadcast extends SwapType {
-
- private static final String TAG = "BonjourBroadcast";
-
- private JmDNS jmdns;
- private ServiceInfo pairService;
-
- public BonjourBroadcast(Context context) {
- super(context);
- }
-
- @Override
- public void start() {
- Utils.debugLog(TAG, "Preparing to start Bonjour service.");
- sendBroadcast(SwapService.EXTRA_STARTING);
-
- InetAddress address = getDeviceAddress();
- if (address == null) {
- Log.e(TAG, "Starting Bonjour service, but couldn't ascertain IP address."
- + " Seems we are not connected to a network.");
- return;
- }
-
- /*
- * a ServiceInfo can only be registered with a single instance
- * of JmDNS, and there is only ever a single LocalHTTPD port to
- * advertise anyway.
- */
- if (pairService != null || jmdns != null) {
- clearCurrentMDNSService();
- }
-
- String repoName = Preferences.get().getLocalRepoName();
- HashMap values = new HashMap<>();
- values.put("path", "/fdroid/repo");
- values.put("name", repoName);
- values.put("fingerprint", FDroidApp.repo.fingerprint);
- String type;
- if (Preferences.get().isLocalRepoHttpsEnabled()) {
- values.put("type", "fdroidrepos");
- type = "_https._tcp.local.";
- } else {
- values.put("type", "fdroidrepo");
- type = "_http._tcp.local.";
- }
- try {
- Utils.debugLog(TAG, "Starting bonjour service...");
- pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
- jmdns = JmDNS.create(address);
- jmdns.registerService(pairService);
- setConnected(true);
- Utils.debugLog(TAG, "... Bounjour service started.");
- } catch (IOException e) {
- Log.e(TAG, "Error while registering jmdns service", e);
- setConnected(false);
- }
- }
-
- @Override
- public void stop() {
- Utils.debugLog(TAG, "Unregistering MDNS service...");
- clearCurrentMDNSService();
- setConnected(false);
- }
-
- private void clearCurrentMDNSService() {
- if (jmdns != null) {
- jmdns.unregisterAllServices();
- Utils.closeQuietly(jmdns);
- pairService = null;
- jmdns = null;
- }
- }
-
- @Override
- public String getBroadcastAction() {
- return SwapService.BONJOUR_STATE_CHANGE;
- }
-
- @Nullable
- private InetAddress getDeviceAddress() {
- if (FDroidApp.ipAddressString != null) {
- try {
- return InetAddress.getByName(FDroidApp.ipAddressString);
- } catch (UnknownHostException ignored) {
- }
- }
-
- return null;
- }
-
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/localrepo/type/SwapType.java b/app/src/full/java/org/fdroid/fdroid/localrepo/type/SwapType.java
deleted file mode 100644
index 8e45aafc8..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/SwapType.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package org.fdroid.fdroid.localrepo.type;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.support.annotation.NonNull;
-import android.support.v4.content.LocalBroadcastManager;
-
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.SwapService;
-
-/**
- * There is lots of common functionality, and a common API among different communication protocols
- * associated with the swap process. This includes Bluetooth visability, Bonjour visability,
- * and the web server which serves info for swapping. This class provides a common API for
- * starting and stopping these services. In addition, it helps with the process of sending broadcast
- * intents in response to the thing starting or stopping.
- */
-public abstract class SwapType {
-
- private static final String TAG = "SwapType";
-
- private boolean isConnected;
-
- @NonNull
- protected final Context context;
-
- public SwapType(@NonNull Context context) {
- this.context = context;
- }
-
- public abstract void start();
-
- public abstract void stop();
-
- protected abstract String getBroadcastAction();
-
- public boolean isDiscoverable() {
- return isConnected();
- }
-
- protected final void setConnected(boolean connected) {
- if (connected) {
- isConnected = true;
- sendBroadcast(SwapService.EXTRA_STARTED);
- } else {
- isConnected = false;
- onStopped();
- sendBroadcast(SwapService.EXTRA_STOPPED);
- }
- }
-
- protected void onStopped() { }
-
- /**
- * Sends either a {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTING},
- * {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STARTED} or
- * {@link org.fdroid.fdroid.localrepo.SwapService#EXTRA_STOPPED} broadcast.
- */
- protected final void sendBroadcast(String extra) {
- if (getBroadcastAction() != null) {
- Intent intent = new Intent(getBroadcastAction());
- intent.putExtra(extra, true);
- Utils.debugLog(TAG, "Sending broadcast " + extra + " from " + getClass().getSimpleName());
- LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
- }
- }
-
- public boolean isConnected() {
- return isConnected;
- }
-
- public void startInBackground() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- start();
- return null;
- }
- }.execute();
- }
-
- private void ensureRunning() {
- if (!isConnected()) {
- start();
- }
- }
-
- public void ensureRunningInBackground() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- ensureRunning();
- return null;
- }
- }.execute();
- }
-
- public void stopInBackground() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- stop();
- return null;
- }
- }.execute();
- }
-
-}
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
deleted file mode 100644
index f76dbe042..000000000
--- a/app/src/full/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java
+++ /dev/null
@@ -1,181 +0,0 @@
-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.SwapService;
-import org.fdroid.fdroid.net.LocalHTTPD;
-import org.fdroid.fdroid.net.WifiStateChangeService;
-import rx.Single;
-import rx.SingleSubscriber;
-import rx.android.schedulers.AndroidSchedulers;
-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;
-
- public WifiSwap(Context context, WifiManager wifiManager) {
- super(context);
- bonjourBroadcast = new BonjourBroadcast(context);
- this.wifiManager = wifiManager;
- }
-
- protected String getBroadcastAction() {
- return SwapService.WIFI_STATE_CHANGE;
- }
-
- public BonjourBroadcast getBonjour() {
- return bonjourBroadcast;
- }
-
- @Override
- public void start() {
- wifiManager.setWifiEnabled(true);
-
- Utils.debugLog(TAG, "Preparing swap webserver.");
- sendBroadcast(SwapService.EXTRA_STARTING);
-
- if (FDroidApp.ipAddressString == null) {
- Log.e(TAG, "Not starting swap webserver, because we don't seem to be connected to a network.");
- setConnected(false);
- }
-
- Single.zip(
- Single.create(getWebServerTask()),
- Single.create(getBonjourTask()),
- new Func2() {
- @Override
- public Boolean call(Boolean webServerTask, Boolean bonjourServiceTask) {
- return bonjourServiceTask && webServerTask;
- }
- })
- .observeOn(AndroidSchedulers.mainThread())
- .subscribeOn(Schedulers.newThread())
- .subscribe(new Action1() {
- @Override
- public void call(Boolean success) {
- setConnected(success);
- }
- },
- new Action1() {
- @Override
- public void call(Throwable throwable) {
- setConnected(false);
- }
- });
- }
-
- /**
- * A task which starts the {@link WifiSwap#bonjourBroadcast} and then emits a `true` value at
- * the end.
- */
- private Single.OnSubscribe getBonjourTask() {
- return new Single.OnSubscribe() {
- @Override
- public void call(SingleSubscriber super Boolean> singleSubscriber) {
- bonjourBroadcast.start();
-
- // TODO: Be more intelligent about failures here so that we can invoke
- // singleSubscriber.onError() in the appropriate circumstances.
- singleSubscriber.onSuccess(true);
- }
- };
- }
-
- /**
- * Constructs a new {@link Thread} for the webserver to run on. If successful, it will also
- * populate the webServerThreadHandler property and bind it to that particular thread. This
- * allows messages to be sent to the webserver thread by posting messages to that handler.
- */
- 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();
- }
- };
- }
-
- @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);
- }
-
- // 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
- // should give enough time for the message we posted above to reach the web server thread
- // and for the webserver to thus be stopped.
- bonjourBroadcast.stop();
- setConnected(false);
- }
-
-}
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/full/java/org/fdroid/fdroid/net/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/net/WifiStateChangeService.java
index db068494e..81f738c2a 100644
--- a/app/src/full/java/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/app/src/full/java/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -59,10 +59,12 @@ public class WifiStateChangeService extends IntentService {
private static final String TAG = "WifiStateChangeService";
public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
+ public static final String EXTRA_STATUS = "wifiStateChangeStatus";
private WifiManager wifiManager;
private static WifiInfoThread wifiInfoThread;
private static int previousWifiState = Integer.MIN_VALUE;
+ private static int wifiState;
public WifiStateChangeService() {
super("WifiStateChangeService");
@@ -86,7 +88,7 @@ public class WifiStateChangeService extends IntentService {
Utils.debugLog(TAG, "WiFi change service started.");
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
- int wifiState = wifiManager.getWifiState();
+ wifiState = wifiManager.getWifiState();
if (ni == null || ni.isConnected()) {
Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState));
if (previousWifiState != wifiState &&
@@ -127,6 +129,7 @@ public class WifiStateChangeService extends IntentService {
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
wifiInfo = wifiManager.getConnectionInfo();
FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress());
+ setSsidFromWifiInfo(wifiInfo);
DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
if (dhcpInfo != null) {
String netmask = formatIpAddress(dhcpInfo.netmask);
@@ -168,17 +171,7 @@ public class WifiStateChangeService extends IntentService {
return;
}
- if (wifiInfo != null) {
- String ssid = wifiInfo.getSSID();
- Utils.debugLog(TAG, "Have wifi info, connected to " + ssid);
- if (ssid != null) {
- FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
- }
- String bssid = wifiInfo.getBSSID();
- if (bssid != null) {
- FDroidApp.bssid = bssid;
- }
- }
+ setSsidFromWifiInfo(wifiInfo);
String scheme;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
@@ -228,10 +221,25 @@ public class WifiStateChangeService extends IntentService {
return;
}
Intent intent = new Intent(BROADCAST);
+ intent.putExtra(EXTRA_STATUS, wifiState);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
}
}
+ private void setSsidFromWifiInfo(WifiInfo wifiInfo) {
+ if (wifiInfo != null) {
+ String ssid = wifiInfo.getSSID();
+ Utils.debugLog(TAG, "Have wifi info, connected to " + ssid);
+ if (ssid != null) {
+ FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
+ }
+ String bssid = wifiInfo.getBSSID();
+ if (bssid != null) {
+ FDroidApp.bssid = bssid;
+ }
+ }
+ }
+
/**
* Search for known Wi-Fi, Hotspot, and local network interfaces and get
* the IP Address info from it. This is necessary because network
diff --git a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
index 66cca3643..801280ca4 100644
--- a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
+++ b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothClient.java
@@ -7,62 +7,26 @@ import android.bluetooth.BluetoothSocket;
import java.io.IOException;
public class BluetoothClient {
-
- @SuppressWarnings("unused")
private static final String TAG = "BluetoothClient";
private final BluetoothDevice device;
- public BluetoothClient(BluetoothDevice device) {
- this.device = device;
- }
-
public BluetoothClient(String macAddress) {
device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
}
public BluetoothConnection openConnection() throws IOException {
- BluetoothSocket socket = null;
BluetoothConnection connection = null;
try {
- socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
+ BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
connection = new BluetoothConnection(socket);
connection.open();
return connection;
- } catch (IOException e1) {
-
+ } finally {
if (connection != null) {
connection.closeQuietly();
}
-
- throw e1;
-
- /*
- Log.e(TAG, "There was an error while establishing Bluetooth connection. Falling back to reflection");
- Class> clazz = socket.getRemoteDevice().getClass();
- Class>[] paramTypes = new Class>[]{Integer.TYPE};
-
- Method method;
- try {
- method = clazz.getMethod("createInsecureRfcommSocket", paramTypes);
- Object[] params = new Object[]{1};
- BluetoothSocket sockFallback = (BluetoothSocket) method.invoke(socket.getRemoteDevice(), params);
-
- BluetoothConnection connection = new BluetoothConnection(sockFallback);
- connection.open();
- return connection;
- } catch (NoSuchMethodException e) {
- throw e1;
- } catch (IllegalAccessException e) {
- throw e1;
- } catch (InvocationTargetException e) {
- throw e1;
- }*/
-
- // Don't catch exceptions this time, let it bubble up as we did our best but don't
- // have anythign else to offer in terms of resolving the problem right now.
}
}
-
}
diff --git a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
index e4410f095..df20f70b8 100644
--- a/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
+++ b/app/src/full/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
@@ -7,7 +7,6 @@ import android.util.Log;
import android.webkit.MimeTypeMap;
import fi.iki.elonen.NanoHTTPD;
import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.type.BluetoothSwap;
import org.fdroid.fdroid.net.bluetooth.httpish.Request;
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
@@ -34,18 +33,9 @@ public class BluetoothServer extends Thread {
private final List clients = new ArrayList<>();
private final File webRoot;
- private final BluetoothSwap swap;
- private boolean isRunning;
- public BluetoothServer(BluetoothSwap swap, File webRoot) {
+ public BluetoothServer(File webRoot) {
this.webRoot = webRoot;
- this.swap = swap;
-
- start();
- }
-
- public boolean isRunning() {
- return isRunning;
}
public void close() {
@@ -64,15 +54,12 @@ public class BluetoothServer extends Thread {
@Override
public void run() {
- isRunning = true;
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
try {
serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
} catch (IOException e) {
Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now", e);
- swap.stop();
- isRunning = false;
return;
}
@@ -102,7 +89,6 @@ public class BluetoothServer extends Thread {
Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients", e);
}
}
- isRunning = false;
}
private static class ClientConnection extends Thread {
diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java b/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java
index 26b3f56b2..96e33b552 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/main/NearbyViewBinder.java
@@ -18,8 +18,8 @@ import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SDCardScannerService;
+import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.TreeUriScannerIntentService;
-import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.io.File;
@@ -75,7 +75,7 @@ class NearbyViewBinder {
ActivityCompat.requestPermissions(activity, new String[]{coarseLocation},
MainActivity.REQUEST_LOCATION_PERMISSIONS);
} else {
- activity.startActivity(new Intent(activity, SwapWorkflowActivity.class));
+ SwapService.start(activity);
}
}
});
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/ConfirmReceiveView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/ConfirmReceiveView.java
deleted file mode 100644
index 7cce11196..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/ConfirmReceiveView.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.data.NewRepoConfig;
-import org.fdroid.fdroid.localrepo.SwapView;
-
-public class ConfirmReceiveView extends SwapView {
-
- private NewRepoConfig config;
-
- public ConfirmReceiveView(Context context) {
- super(context);
- }
-
- public ConfirmReceiveView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public ConfirmReceiveView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public ConfirmReceiveView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- findViewById(R.id.no_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().denySwap();
- }
- });
-
- findViewById(R.id.yes_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().swapWith(config);
- }
- });
- }
-
- public void setup(NewRepoConfig config) {
- this.config = config;
- }
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/ConnectingView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/ConnectingView.java
deleted file mode 100644
index 9a1208bff..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/ConnectingView.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.support.v4.content.LocalBroadcastManager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.UpdateService;
-import org.fdroid.fdroid.localrepo.SwapView;
-
-public class ConnectingView extends SwapView {
-
- @SuppressWarnings("unused")
- private static final String TAG = "ConnectingView";
-
- public ConnectingView(Context context) {
- super(context);
- }
-
- public ConnectingView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @TargetApi(11)
- public ConnectingView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public ConnectingView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- ((TextView) findViewById(R.id.heading)).setText(R.string.swap_connecting);
- findViewById(R.id.back).setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().showIntro();
- }
- });
-
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- repoUpdateReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- prepareSwapReceiver, new IntentFilter(SwapWorkflowActivity.PrepareSwapRepo.ACTION));
- }
-
- /**
- * Remove relevant listeners/receivers/etc so that they do not receive and process events
- * when this view is not in use.
- */
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(repoUpdateReceiver);
- LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(prepareSwapReceiver);
- }
-
- private final BroadcastReceiver repoUpdateReceiver = new ConnectSwapReceiver();
- private final BroadcastReceiver prepareSwapReceiver = new PrepareSwapReceiver();
-
- /**
- * Listens for feedback about a local repository being prepared:
- * * Apk files copied to the LocalHTTPD webroot
- * * index.html file prepared
- * * Icons will be copied to the webroot in the background and so are not part of this process.
- */
- class PrepareSwapReceiver extends Receiver {
-
- @Override
- protected String getMessageExtra() {
- return SwapWorkflowActivity.PrepareSwapRepo.EXTRA_MESSAGE;
- }
-
- protected int getType(Intent intent) {
- return intent.getIntExtra(SwapWorkflowActivity.PrepareSwapRepo.EXTRA_TYPE, -1);
- }
-
- @Override
- protected boolean isComplete(Intent intent) {
- return getType(intent) == SwapWorkflowActivity.PrepareSwapRepo.TYPE_COMPLETE;
- }
-
- @Override
- protected boolean isError(Intent intent) {
- return getType(intent) == SwapWorkflowActivity.PrepareSwapRepo.TYPE_ERROR;
- }
-
- @Override
- protected void onComplete() {
- getActivity().onLocalRepoPrepared();
- }
- }
-
- /**
- * Listens for feedback about a repo update process taking place.
- * Tracks an index.jar download and show the progress messages
- */
- class ConnectSwapReceiver extends Receiver {
-
- @Override
- protected String getMessageExtra() {
- return UpdateService.EXTRA_MESSAGE;
- }
-
- protected int getStatusCode(Intent intent) {
- return intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1);
- }
-
- @Override
- protected boolean isComplete(Intent intent) {
- int status = getStatusCode(intent);
- return status == UpdateService.STATUS_COMPLETE_AND_SAME ||
- status == UpdateService.STATUS_COMPLETE_WITH_CHANGES;
- }
-
- @Override
- protected boolean isError(Intent intent) {
- int status = getStatusCode(intent);
- return status == UpdateService.STATUS_ERROR_GLOBAL ||
- status == UpdateService.STATUS_ERROR_LOCAL ||
- status == UpdateService.STATUS_ERROR_LOCAL_SMALL;
- }
-
- @Override
- protected void onComplete() {
- getActivity().inflateSwapView(R.layout.swap_success);
- }
-
- }
-
- abstract class Receiver extends BroadcastReceiver {
-
- protected abstract String getMessageExtra();
-
- protected abstract boolean isComplete(Intent intent);
-
- protected abstract boolean isError(Intent intent);
-
- protected abstract void onComplete();
-
- @Override
- public void onReceive(Context context, Intent intent) {
-
- TextView progressText = (TextView) findViewById(R.id.heading);
- ProgressBar progressBar = findViewById(R.id.progress_bar);
- TextView errorText = (TextView) findViewById(R.id.error);
- Button backButton = (Button) findViewById(R.id.back);
-
- String message;
- if (intent.hasExtra(getMessageExtra())) {
- message = intent.getStringExtra(getMessageExtra());
- if (message != null) {
- progressText.setText(message);
- }
- }
-
- progressText.setVisibility(View.VISIBLE);
- progressBar.setVisibility(View.VISIBLE);
- errorText.setVisibility(View.GONE);
- backButton.setVisibility(View.GONE);
-
- if (isError(intent)) {
- progressText.setVisibility(View.GONE);
- progressBar.setVisibility(View.GONE);
- errorText.setVisibility(View.VISIBLE);
- backButton.setVisibility(View.VISIBLE);
- return;
- }
-
- if (isComplete(intent)) {
- onComplete();
- }
- }
- }
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/JoinWifiView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/JoinWifiView.java
deleted file mode 100644
index ac8454e47..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/JoinWifiView.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.wifi.WifiManager;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.localrepo.SwapView;
-import org.fdroid.fdroid.net.WifiStateChangeService;
-
-public class JoinWifiView extends SwapView {
-
- public JoinWifiView(Context context) {
- super(context);
- }
-
- public JoinWifiView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public JoinWifiView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public JoinWifiView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- openAvailableNetworks();
- }
- });
- refreshWifiState();
-
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- onWifiStateChange,
- new IntentFilter(WifiStateChangeService.BROADCAST)
- );
- }
-
- /**
- * Remove relevant listeners/receivers/etc so that they do not receive and process events
- * when this view is not in use.
- */
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(onWifiStateChange);
- }
-
- // TODO: Listen for "Connecting..." state and reflect that in the view too.
- private void refreshWifiState() {
- TextView descriptionView = (TextView) findViewById(R.id.text_description);
- ImageView wifiIcon = (ImageView) findViewById(R.id.wifi_icon);
- TextView ssidView = (TextView) findViewById(R.id.wifi_ssid);
- TextView tapView = (TextView) findViewById(R.id.wifi_available_networks_prompt);
- if (TextUtils.isEmpty(FDroidApp.bssid) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) {
- // empty bssid with an ipAddress means hotspot mode
- descriptionView.setText(R.string.swap_join_this_hotspot);
- wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.hotspot));
- ssidView.setText(R.string.swap_active_hotspot);
- tapView.setText(R.string.swap_switch_to_wifi);
- } else if (TextUtils.isEmpty(FDroidApp.ssid)) {
- // not connected to or setup with any wifi network
- descriptionView.setText(R.string.swap_join_same_wifi);
- wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.wifi));
- ssidView.setText(R.string.swap_no_wifi_network);
- tapView.setText(R.string.swap_view_available_networks);
- } else {
- // connected to a regular wifi network
- descriptionView.setText(R.string.swap_join_same_wifi);
- wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.wifi));
- ssidView.setText(FDroidApp.ssid);
- tapView.setText(R.string.swap_view_available_networks);
- }
- }
-
- private void openAvailableNetworks() {
- getActivity().startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK));
- }
-
- private final BroadcastReceiver onWifiStateChange = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- refreshWifiState();
- }
- };
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/NfcView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/NfcView.java
deleted file mode 100644
index 43c8989a0..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/NfcView.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.localrepo.SwapView;
-
-public class NfcView extends SwapView {
-
- public NfcView(Context context) {
- super(context);
- }
-
- public NfcView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public NfcView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public NfcView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- CheckBox dontShowAgain = (CheckBox) findViewById(R.id.checkbox_dont_show);
- dontShowAgain.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- Preferences.get().setShowNfcDuringSwap(!isChecked);
- }
- });
- }
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SelectAppsView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SelectAppsView.java
index 9068be658..d45ffdb5f 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SelectAppsView.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SelectAppsView.java
@@ -29,6 +29,7 @@ import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.SwapView;
@@ -51,10 +52,6 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall
super(context, attrs, defStyleAttr, defStyleRes);
}
- private SwapService getState() {
- return getActivity().getState();
- }
-
private ListView listView;
private AppListAdapter adapter;
@@ -82,14 +79,14 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall
private void toggleAppSelected(int position) {
Cursor c = (Cursor) adapter.getItem(position);
String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME));
- if (getState().hasSelectedPackage(packageName)) {
- getState().deselectPackage(packageName);
+ if (getActivity().getSwapService().hasSelectedPackage(packageName)) {
+ getActivity().getSwapService().deselectPackage(packageName);
adapter.updateCheckedIndicatorView(position, false);
} else {
- getState().selectPackage(packageName);
+ getActivity().getSwapService().selectPackage(packageName);
adapter.updateCheckedIndicatorView(position, true);
}
-
+ LocalRepoService.create(getContext(), getActivity().getSwapService().getAppsToSwap());
}
@Override
@@ -116,8 +113,8 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall
for (int i = 0; i < listView.getCount(); i++) {
Cursor c = (Cursor) listView.getItemAtPosition(i);
String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME));
- getState().ensureFDroidSelected();
- for (String selected : getState().getAppsToSwap()) {
+ getActivity().getSwapService().ensureFDroidSelected();
+ for (String selected : getActivity().getSwapService().getAppsToSwap()) {
if (TextUtils.equals(packageName, selected)) {
listView.setItemChecked(i, true);
}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SendFDroidView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SendFDroidView.java
deleted file mode 100644
index 30ba74da7..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SendFDroidView.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.LightingColorFilter;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.QrGenAsyncTask;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.SwapView;
-import org.fdroid.fdroid.net.WifiStateChangeService;
-import org.fdroid.fdroid.views.swap.device.camera.CameraCharacteristicsChecker;
-
-public class SendFDroidView extends SwapView {
-
- private static final String TAG = "SendFDroidView";
-
- public SendFDroidView(Context context) {
- super(context);
- }
-
- public SendFDroidView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public SendFDroidView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public SendFDroidView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- setUIFromWifi();
- setUpWarningMessageQrScan();
-
- ImageView qrImage = (ImageView) findViewById(R.id.wifi_qr_code);
-
- // Replace all blacks with the background blue.
- qrImage.setColorFilter(new LightingColorFilter(0xffffffff, getResources().getColor(R.color.swap_blue)));
-
- Button useBluetooth = (Button) findViewById(R.id.btn_use_bluetooth);
- useBluetooth.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().showIntro();
- getActivity().sendFDroidBluetooth();
- }
- });
-
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- onWifiStateChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
- }
-
- private void setUpWarningMessageQrScan() {
- final View qrWarningMessage = findViewById(R.id.warning_qr_scanner);
- final boolean hasAutofocus = CameraCharacteristicsChecker.getInstance(getContext()).hasAutofocus();
- final int visiblity = hasAutofocus ? GONE : VISIBLE;
- qrWarningMessage.setVisibility(visiblity);
- }
-
-
- /**
- * Remove relevant listeners/receivers/etc so that they do not receive and process events
- * when this view is not in use.
- */
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(onWifiStateChanged);
- }
-
- @SuppressLint("HardwareIds")
- private void setUIFromWifi() {
- if (TextUtils.isEmpty(FDroidApp.repo.address)) {
- return;
- }
-
- String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://";
-
- // the fingerprint is not useful on the button label
- String qrUriString = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port;
- TextView ipAddressView = (TextView) findViewById(R.id.device_ip_address);
- ipAddressView.setText(qrUriString);
-
- Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString);
- new QrGenAsyncTask(getActivity(), R.id.wifi_qr_code).execute(qrUriString);
-
- }
-
- private final BroadcastReceiver onWifiStateChanged = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- setUIFromWifi();
- }
- };
-
-}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java
index 09c1558d4..1da4701e3 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/StartSwapView.java
@@ -26,26 +26,18 @@ import cc.mvdan.accesspoint.WifiApControl;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.BluetoothManager;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.SwapView;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.WifiStateChangeService;
-import rx.Subscriber;
-import rx.Subscription;
import java.util.ArrayList;
@SuppressWarnings("LineLength")
public class StartSwapView extends SwapView {
-
private static final String TAG = "StartSwapView";
- // TODO: Is there a way to guarantee which of these constructors the inflater will call?
- // Especially on different API levels? It would be nice to only have the one which accepts
- // a Context, but I'm not sure if that is correct or not. As it stands, this class provides
- // constructors which match each of the ones available in the parent class.
- // The same is true for the other views in the swap process too.
-
public StartSwapView(Context context) {
super(context);
}
@@ -63,7 +55,7 @@ public class StartSwapView extends SwapView {
super(context, attrs, defStyleAttr, defStyleRes);
}
- private class PeopleNearbyAdapter extends ArrayAdapter {
+ class PeopleNearbyAdapter extends ArrayAdapter {
PeopleNearbyAdapter(Context context) {
super(context, 0, new ArrayList());
@@ -83,19 +75,12 @@ public class StartSwapView extends SwapView {
return convertView;
}
-
- }
-
- private SwapService getManager() {
- return getActivity().getState();
}
@Nullable /* Emulators typically don't have bluetooth adapters */
private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
- private SwitchCompat wifiSwitch;
private SwitchCompat bluetoothSwitch;
- private TextView textWifiVisible;
private TextView viewBluetoothId;
private TextView textBluetoothVisible;
private TextView viewWifiId;
@@ -106,55 +91,18 @@ public class StartSwapView extends SwapView {
private PeopleNearbyAdapter peopleNearbyAdapter;
- /**
- * When peers are emitted by the peer finder, add them to the adapter
- * so that they will show up in the list of peers.
- */
- private final Subscriber onPeerFound = new Subscriber() {
-
- @Override
- public void onCompleted() {
- uiShowNotSearchingForPeers();
- }
-
- @Override
- public void onError(Throwable e) {
- uiShowNotSearchingForPeers();
- }
-
- @Override
- public void onNext(Peer peer) {
- Utils.debugLog(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
- peopleNearbyAdapter.add(peer);
- }
- };
-
- private Subscription peerFinderSubscription;
-
/**
* Remove relevant listeners/subscriptions/etc so that they do not receive and process events
* when this view is not in use.
*
- * TODO: Not sure if this is the best place to handle being removed from the view.
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- if (peerFinderSubscription != null) {
- peerFinderSubscription.unsubscribe();
- peerFinderSubscription = null;
- }
-
- if (wifiSwitch != null) {
- wifiSwitch.setOnCheckedChangeListener(null);
- }
-
if (bluetoothSwitch != null) {
bluetoothSwitch.setOnCheckedChangeListener(null);
}
- LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiSwapStateChanged);
- LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onBluetoothSwapStateChanged);
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiNetworkChanged);
}
@@ -162,15 +110,10 @@ public class StartSwapView extends SwapView {
protected void onFinishInflate() {
super.onFinishInflate();
- if (peerFinderSubscription == null) {
- peerFinderSubscription = getManager().scanForPeers().subscribe(onPeerFound);
- }
-
uiInitPeers();
uiInitBluetooth();
uiInitWifi();
uiInitButtons();
- uiShowSearchingForPeers();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
onWifiNetworkChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
@@ -190,13 +133,6 @@ public class StartSwapView extends SwapView {
getActivity().sendFDroid();
}
});
-
- findViewById(R.id.btn_qr_scanner).setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().startQrWorkflow();
- }
- });
}
/**
@@ -211,6 +147,7 @@ public class StartSwapView extends SwapView {
peopleNearbyAdapter = new PeopleNearbyAdapter(getContext());
peopleNearbyList.setAdapter(peopleNearbyAdapter);
+ peopleNearbyAdapter.addAll(getActivity().getSwapService().getActivePeers());
peopleNearbyList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -221,11 +158,6 @@ public class StartSwapView extends SwapView {
});
}
- private void uiShowSearchingForPeers() {
- peopleNearbyText.setText(getContext().getString(R.string.swap_scanning_for_peers));
- peopleNearbyProgress.setVisibility(View.VISIBLE);
- }
-
private void uiShowNotSearchingForPeers() {
peopleNearbyProgress.setVisibility(View.GONE);
if (peopleNearbyList.getAdapter().getCount() > 0) {
@@ -238,68 +170,22 @@ public class StartSwapView extends SwapView {
private void uiInitBluetooth() {
if (bluetooth != null) {
- textBluetoothVisible = (TextView) findViewById(R.id.bluetooth_visible);
-
viewBluetoothId = (TextView) findViewById(R.id.device_id_bluetooth);
viewBluetoothId.setText(bluetooth.getName());
viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE);
- int textResource = getManager().isBluetoothDiscoverable() ? R.string.swap_visible_bluetooth : R.string.swap_not_visible_bluetooth;
- textBluetoothVisible.setText(textResource);
+ textBluetoothVisible = findViewById(R.id.bluetooth_visible);
bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth);
- Utils.debugLog(TAG, getManager().isBluetoothDiscoverable()
- ? "Initially marking switch as checked, because Bluetooth is discoverable."
- : "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
- setBluetoothSwitchState(getManager().isBluetoothDiscoverable(), true);
-
- LocalBroadcastManager.getInstance(getContext()).registerReceiver(onBluetoothSwapStateChanged, new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE));
-
+ bluetoothSwitch.setChecked(SwapService.getBluetoothVisibleUserPreference());
+ bluetoothSwitch.setEnabled(true);
+ bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
} else {
findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
}
}
- /**
- * @see StartSwapView#onWifiSwapStateChanged
- */
- private final BroadcastReceiver onBluetoothSwapStateChanged = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
- Utils.debugLog(TAG, "Bluetooth service is starting (setting toggle to disabled, not checking because we will wait for an intent that bluetooth is actually enabled)");
- bluetoothSwitch.setEnabled(false);
- textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth);
- // bluetoothSwitch.setChecked(true);
- } else {
- if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
- Utils.debugLog(TAG, "Bluetooth service has started (updating text to visible, but not marking as checked).");
- textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
- bluetoothSwitch.setEnabled(true);
- // bluetoothSwitch.setChecked(true);
- } else {
- Utils.debugLog(TAG, "Bluetooth service has stopped (setting switch to not-visible).");
- textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
- setBluetoothSwitchState(false, true);
- }
- }
- }
- };
-
- /**
- * @see StartSwapView#setWifiSwitchState(boolean, boolean)
- */
- private void setBluetoothSwitchState(boolean isChecked, boolean isEnabled) {
- bluetoothSwitch.setOnCheckedChangeListener(null);
- bluetoothSwitch.setChecked(isChecked);
- bluetoothSwitch.setEnabled(isEnabled);
- bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
- }
-
- /**
- * @see StartSwapView#onWifiSwitchToggled
- */
private final CompoundButton.OnCheckedChangeListener onBluetoothSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@@ -312,7 +198,7 @@ public class StartSwapView extends SwapView {
// TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
} else {
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, disabling Bluetooth swap.");
- getManager().getBluetoothSwap().stop();
+ BluetoothManager.stop(getContext());
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
viewBluetoothId.setVisibility(View.GONE);
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully.");
@@ -326,106 +212,9 @@ public class StartSwapView extends SwapView {
viewWifiId = (TextView) findViewById(R.id.device_id_wifi);
viewWifiNetwork = (TextView) findViewById(R.id.wifi_network);
- wifiSwitch = (SwitchCompat) findViewById(R.id.switch_wifi);
- wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
- setWifiSwitchState(getManager().isBonjourDiscoverable(), true);
-
- textWifiVisible = (TextView) findViewById(R.id.wifi_visible);
- int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
- textWifiVisible.setText(textResource);
-
- // Note that this is only listening for the WifiSwap, whereas we start both the WifiSwap
- // and the Bonjour service at the same time. Technically swap will work fine without
- // Bonjour, and that is more of a convenience. Thus, we should show feedback once wifi
- // is ready, even if Bonjour is not yet.
- LocalBroadcastManager.getInstance(getContext()).registerReceiver(onWifiSwapStateChanged,
- new IntentFilter(SwapService.WIFI_STATE_CHANGE));
-
- viewWifiNetwork.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().promptToSelectWifiNetwork();
- }
- });
-
uiUpdateWifiNetwork();
}
- /**
- * When the WiFi swap service is started or stopped, update the UI appropriately.
- * This includes both the in-transit states of "Starting" and "Stopping". In these two cases,
- * the UI should be disabled to prevent the user quickly switching back and forth - causing
- * multiple start/stop actions to be sent to the swap service.
- */
- private final BroadcastReceiver onWifiSwapStateChanged = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
- Utils.debugLog(TAG, "WiFi service is starting (setting toggle to checked, but disabled).");
- textWifiVisible.setText(R.string.swap_setting_up_wifi);
- setWifiSwitchState(true, false);
- } else if (intent.hasExtra(SwapService.EXTRA_STOPPING)) {
- Utils.debugLog(TAG, "WiFi service is stopping (setting toggle to unchecked and disabled).");
- textWifiVisible.setText(R.string.swap_stopping_wifi);
- setWifiSwitchState(false, false);
- } else {
- if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
- Utils.debugLog(TAG, "WiFi service has started (setting toggle to visible).");
- textWifiVisible.setText(R.string.swap_visible_wifi);
- setWifiSwitchState(true, true);
- } else {
- Utils.debugLog(TAG, "WiFi service has stopped (setting toggle to not-visible).");
- textWifiVisible.setText(R.string.swap_not_visible_wifi);
- setWifiSwitchState(false, true);
- }
- }
- uiUpdateWifiNetwork();
- }
- };
-
- /**
- * Helper function to set the "enable wifi" switch, but prevents the listeners from
- * being notified. This enables the UI to be updated without triggering further enable/disable
- * events being queued.
- *
- * This is required because the SwitchCompat and its parent classes will always try to notify
- * their listeners if there is one (e.g. http://stackoverflow.com/a/15523518).
- *
- * The fact that this method also deals with enabling/disabling the switch is more of a convenience
- * Nigh on all times this UI wants to change the state of the switch, it is also interested in
- * ensuring the enabled state of the switch.
- */
- private void setWifiSwitchState(boolean isChecked, boolean isEnabled) {
- wifiSwitch.setOnCheckedChangeListener(null);
- wifiSwitch.setChecked(isChecked);
- wifiSwitch.setEnabled(isEnabled);
- wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
- }
-
- /**
- * When the wifi switch is:
- *
- * Toggled on: Ask the swap service to ensure wifi swap is running.
- * Toggled off: Ask the swap service to prevent the wifi swap service from running.
- *
- * Both of these actions will be performed in a background thread which will send broadcast
- * intents when they are completed.
- */
- private final CompoundButton.OnCheckedChangeListener onWifiSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- if (isChecked) {
- Utils.debugLog(TAG, "Received onCheckChanged(true) for WiFi swap, asking in background thread to ensure WiFi swap is running.");
- getManager().getWifiSwap().ensureRunningInBackground();
- } else {
- Utils.debugLog(TAG, "Received onCheckChanged(false) for WiFi swap, disabling WiFi swap in background thread.");
- getManager().getWifiSwap().stopInBackground();
- }
- SwapService.putWifiVisibleUserPreference(isChecked);
- uiUpdateWifiNetwork();
- }
- };
-
private void uiUpdateWifiNetwork() {
viewWifiId.setText(FDroidApp.ipAddressString);
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapSuccessView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapSuccessView.java
index 5fd5ac97d..e642a32a4 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapSuccessView.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapSuccessView.java
@@ -2,6 +2,7 @@ package org.fdroid.fdroid.views.swap;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -11,7 +12,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
@@ -21,6 +21,7 @@ import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.CursorAdapter;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,7 +32,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.ImageLoader;
-import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
@@ -41,14 +41,20 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
+import org.fdroid.fdroid.installer.InstallManagerService;
+import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.localrepo.SwapView;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import java.util.List;
-import java.util.Timer;
-import java.util.TimerTask;
+/**
+ * This is a view that shows a listing of all apps in the swap repo that this
+ * just connected to. The app listing and search should be replaced by
+ * {@link org.fdroid.fdroid.views.apps.AppListActivity}'s plumbing.
+ */
+// TODO merge this with AppListActivity, perhaps there could be AppListView?
public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCallbacks {
private static final String TAG = "SwapAppsView";
@@ -71,19 +77,11 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
private Repo repo;
private AppListAdapter adapter;
- private String currentFilterString;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- repo = getActivity().getState().getPeerRepo();
-
- /*
- if (repo == null) {
- TODO: Uh oh, something stuffed up for this to happen.
- TODO: What is the best course of action from here?
- }
- */
+ repo = getActivity().getSwapService().getPeerRepo();
adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query(
AppProvider.getRepoUri(repo), AppMetadataTable.Cols.ALL, null, null, null));
@@ -95,8 +93,6 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
pollForUpdatesReceiver, new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
-
- schedulePollForUpdates();
}
/**
@@ -110,29 +106,7 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(pollForUpdatesReceiver);
}
- private void pollForUpdates() {
- if (adapter.getCount() > 1 ||
- (adapter.getCount() == 1 && !new App((Cursor) adapter.getItem(0)).packageName.equals(BuildConfig.APPLICATION_ID))) { // NOCHECKSTYLE LineLength
- Utils.debugLog(TAG, "Not polling for new apps from swap repo, because we already have more than one.");
- return;
- }
-
- Utils.debugLog(TAG, "Polling swap repo to see if it has any updates.");
- getActivity().getService().refreshSwap();
- }
-
- private void schedulePollForUpdates() {
- Utils.debugLog(TAG, "Scheduling poll for updated swap repo in 5 seconds.");
- new Timer().schedule(new TimerTask() {
- @Override
- public void run() {
- Looper.prepare();
- pollForUpdates();
- Looper.loop();
- }
- }, 5000);
- }
-
+ @NonNull
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
Uri uri = TextUtils.isEmpty(currentFilterString)
@@ -144,12 +118,12 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
}
@Override
- public void onLoadFinished(Loader loader, Cursor cursor) {
+ public void onLoadFinished(@NonNull Loader loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
@Override
- public void onLoaderReset(Loader loader) {
+ public void onLoaderReset(@NonNull Loader loader) {
adapter.swapCursor(null);
}
@@ -172,7 +146,7 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
TextView statusInstalled;
TextView statusIncompatible;
- private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
+ private class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
@@ -194,9 +168,14 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
}
break;
case Downloader.ACTION_COMPLETE:
+ localBroadcastManager.unregisterReceiver(this);
resetView();
+ statusInstalled.setText(R.string.installing);
+ statusInstalled.setVisibility(View.VISIBLE);
+ btnInstall.setVisibility(View.GONE);
break;
case Downloader.ACTION_INTERRUPTED:
+ localBroadcastManager.unregisterReceiver(this);
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
+ " " + intent.getDataString();
@@ -210,9 +189,8 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
default:
throw new RuntimeException("intent action not handled!");
}
-
}
- };
+ }
private final ContentObserver appObserver = new ContentObserver(new Handler()) {
@Override
@@ -244,9 +222,48 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
}
if (apk != null) {
- // TODO unregister receivers? or will they just die with this instance
- IntentFilter downloadFilter = DownloaderService.getIntentFilter(apk.getCanonicalUrl());
- localBroadcastManager.registerReceiver(downloadReceiver, downloadFilter);
+ localBroadcastManager.registerReceiver(new DownloadReceiver(),
+ DownloaderService.getIntentFilter(apk.getCanonicalUrl()));
+ localBroadcastManager.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Installer.ACTION_INSTALL_STARTED:
+ statusInstalled.setText(R.string.installing);
+ statusInstalled.setVisibility(View.VISIBLE);
+ btnInstall.setVisibility(View.GONE);
+ progressView.setIndeterminate(true);
+ progressView.setVisibility(View.VISIBLE);
+ break;
+ case Installer.ACTION_INSTALL_USER_INTERACTION:
+ PendingIntent installPendingIntent =
+ intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
+ try {
+ installPendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "PI canceled", e);
+ }
+ break;
+ case Installer.ACTION_INSTALL_COMPLETE:
+ localBroadcastManager.unregisterReceiver(this);
+ statusInstalled.setText(R.string.app_installed);
+ statusInstalled.setVisibility(View.VISIBLE);
+ btnInstall.setVisibility(View.GONE);
+ progressView.setVisibility(View.GONE);
+ break;
+ case Installer.ACTION_INSTALL_INTERRUPTED:
+ localBroadcastManager.unregisterReceiver(this);
+ statusInstalled.setVisibility(View.GONE);
+ btnInstall.setVisibility(View.VISIBLE);
+ progressView.setVisibility(View.GONE);
+ String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
+ if (errorMessage != null) {
+ Toast.makeText(getContext(), errorMessage, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ }
+ }, Installer.getInstallIntentFilter(apk.getCanonicalUrl()));
}
// NOTE: Instead of continually unregistering and re-registering the observer
@@ -261,6 +278,25 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
resetView();
}
+ private final OnClickListener cancelListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (apk != null) {
+ InstallManagerService.cancel(getContext(), apk.getCanonicalUrl());
+ }
+ }
+ };
+
+ private final OnClickListener installListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (apk != null && (app.hasUpdates() || app.compatible)) {
+ showProgress();
+ InstallManagerService.queue(getContext(), app, apk);
+ }
+ }
+ };
+
private void resetView() {
if (app == null) {
@@ -279,39 +315,38 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
if (app.hasUpdates()) {
btnInstall.setText(R.string.menu_upgrade);
btnInstall.setVisibility(View.VISIBLE);
+ btnInstall.setOnClickListener(installListener);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
} else if (app.isInstalled(getContext())) {
btnInstall.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.VISIBLE);
+ statusInstalled.setText(R.string.app_installed);
} else if (!app.compatible) {
btnInstall.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.VISIBLE);
statusInstalled.setVisibility(View.GONE);
+ } else if (progressView.getVisibility() == View.VISIBLE) {
+ btnInstall.setText(R.string.cancel);
+ btnInstall.setVisibility(View.VISIBLE);
+ btnInstall.setOnClickListener(cancelListener);
+ statusIncompatible.setVisibility(View.GONE);
+ statusInstalled.setVisibility(View.GONE);
} else {
btnInstall.setText(R.string.menu_install);
btnInstall.setVisibility(View.VISIBLE);
+ btnInstall.setOnClickListener(installListener);
statusIncompatible.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
}
-
- OnClickListener installListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (apk != null && (app.hasUpdates() || app.compatible)) {
- getActivity().install(app, apk);
- showProgress();
- }
- }
- };
-
- btnInstall.setOnClickListener(installListener);
}
private void showProgress() {
+ btnInstall.setText(R.string.cancel);
+ btnInstall.setVisibility(View.VISIBLE);
+ btnInstall.setOnClickListener(cancelListener);
progressView.setVisibility(View.VISIBLE);
- btnInstall.setVisibility(View.GONE);
statusInstalled.setVisibility(View.GONE);
statusIncompatible.setVisibility(View.GONE);
}
@@ -372,17 +407,7 @@ public class SwapSuccessView extends SwapView implements LoaderManager.LoaderCal
}
});
break;
-
- case UpdateService.STATUS_ERROR_GLOBAL:
- // TODO: Well, if we can't get the index, we probably can't swapp apps.
- // Tell the user something helpful?
- break;
-
- case UpdateService.STATUS_COMPLETE_AND_SAME:
- schedulePollForUpdates();
- break;
}
}
};
-
}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index ee92c4dca..7909a95e2 100644
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/app/src/full/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -2,17 +2,17 @@ package org.fdroid.fdroid.views.swap;
import android.annotation.TargetApi;
import android.app.Activity;
-import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.net.wifi.WifiManager;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -26,6 +26,7 @@ import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView;
+import android.support.v7.widget.SwitchCompat;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
@@ -35,6 +36,13 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import cc.mvdan.accesspoint.WifiApControl;
@@ -44,29 +52,38 @@ import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.QrGenAsyncTask;
import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.data.Apk;
-import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.NewRepoConfig;
-import org.fdroid.fdroid.installer.InstallManagerService;
-import org.fdroid.fdroid.installer.Installer;
-import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.localrepo.BluetoothManager;
+import org.fdroid.fdroid.localrepo.BonjourManager;
+import org.fdroid.fdroid.localrepo.LocalHTTPDManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.SwapView;
+import org.fdroid.fdroid.localrepo.peers.BluetoothPeer;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.BluetoothDownloader;
+import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.HttpDownloader;
+import org.fdroid.fdroid.net.WifiStateChangeService;
+import org.fdroid.fdroid.views.main.MainActivity;
+import org.fdroid.fdroid.views.swap.device.camera.CameraCharacteristicsChecker;
-import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
+import static org.fdroid.fdroid.views.main.MainActivity.ACTION_REQUEST_SWAP;
+
/**
* This activity will do its best to show the most relevant screen about swapping to the user.
* The problem comes when there are two competing goals - 1) Show the user a list of apps from another
@@ -84,35 +101,33 @@ public class SwapWorkflowActivity extends AppCompatActivity {
* among each other offering swaps.
*/
public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap";
- public static final String EXTRA_CONFIRM = "EXTRA_CONFIRM";
-
- /**
- * Ensure that we don't try to handle specific intents more than once in onResume()
- * (e.g. the "Do you want to swap back with ..." intent).
- */
- public static final String EXTRA_SWAP_INTENT_HANDLED = "swapIntentHandled";
private ViewGroup container;
- private static final int CONNECT_TO_SWAP = 1;
private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SWAP = 2;
private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3;
private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SEND = 4;
private static final int REQUEST_WRITE_SETTINGS_PERMISSION = 5;
+ private static final int STEP_INTRO = 1; // TODO remove this special case, only use layoutResIds
private Toolbar toolbar;
private SwapView currentView;
private boolean hasPreparedLocalRepo;
- private PrepareSwapRepo updateSwappableAppsTask;
+ private boolean newIntent;
private NewRepoConfig confirmSwapConfig;
private LocalBroadcastManager localBroadcastManager;
private WifiManager wifiManager;
+ private BluetoothAdapter bluetoothAdapter;
+
+ @LayoutRes
+ private int currentSwapViewLayoutRes = STEP_INTRO;
public static void requestSwap(Context context, String repo) {
- Uri repoUri = Uri.parse(repo);
- Intent intent = new Intent(context, SwapWorkflowActivity.class);
- intent.setData(repoUri);
- intent.putExtra(EXTRA_CONFIRM, true);
+ requestSwap(context, Uri.parse(repo));
+ }
+
+ public static void requestSwap(Context context, Uri uri) {
+ Intent intent = new Intent(MainActivity.ACTION_REQUEST_SWAP, uri, context, SwapWorkflowActivity.class);
intent.putExtra(EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
@@ -122,17 +137,14 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
- Utils.debugLog(TAG, "Swap service connected. Will hold onto it so we can talk to it regularly.");
service = ((SwapService.Binder) binder).getService();
showRelevantView();
}
- // TODO: What causes this? Do we need to stop swapping explicitly when this is invoked?
@Override
public void onServiceDisconnected(ComponentName className) {
- Utils.debugLog(TAG, "Swap service disconnected");
+ finish();
service = null;
- // TODO: What to do about the UI in this instance?
}
};
@@ -140,18 +152,14 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private SwapService service;
@NonNull
- public SwapService getService() {
- if (service == null) {
- // *Slightly* more informative than a null-pointer error that would otherwise happen.
- throw new IllegalStateException("Trying to access swap service before it was initialized.");
- }
+ public SwapService getSwapService() {
return service;
}
@Override
public void onBackPressed() {
- if (currentView.getLayoutResId() == SwapService.STEP_INTRO) {
- SwapService.stop(this); // TODO SwapService should always be running, while swap is running
+ if (currentView.getLayoutResId() == STEP_INTRO) {
+ SwapService.stop(this);
finish();
} else {
// TODO: Currently StartSwapView is handleed by the SwapWorkflowActivity as a special case, where
@@ -161,39 +169,34 @@ public class SwapWorkflowActivity extends AppCompatActivity {
int nextStep = -1;
switch (currentView.getLayoutResId()) {
case R.layout.swap_confirm_receive:
- nextStep = SwapService.STEP_INTRO;
+ nextStep = STEP_INTRO;
break;
case R.layout.swap_connecting:
nextStep = R.layout.swap_select_apps;
break;
- case R.layout.swap_initial_loading:
- nextStep = R.layout.swap_join_wifi;
- break;
case R.layout.swap_join_wifi:
- nextStep = SwapService.STEP_INTRO;
+ nextStep = STEP_INTRO;
break;
case R.layout.swap_nfc:
nextStep = R.layout.swap_join_wifi;
break;
case R.layout.swap_select_apps:
- // TODO: The STEP_JOIN_WIFI step isn't shown first, need to make it
- // so that it is, or so that this doesn't go back there.
- nextStep = getState().isConnectingWithPeer() ? SwapService.STEP_INTRO : R.layout.swap_join_wifi;
+ nextStep = getSwapService().isConnectingWithPeer() ? STEP_INTRO : R.layout.swap_join_wifi;
break;
case R.layout.swap_send_fdroid:
- nextStep = SwapService.STEP_INTRO;
+ nextStep = STEP_INTRO;
break;
case R.layout.swap_start_swap:
- nextStep = SwapService.STEP_INTRO;
+ nextStep = STEP_INTRO;
break;
case R.layout.swap_success:
- nextStep = SwapService.STEP_INTRO;
+ nextStep = STEP_INTRO;
break;
case R.layout.swap_wifi_qr:
nextStep = R.layout.swap_join_wifi;
break;
}
- getService().setCurrentView(nextStep);
+ currentSwapViewLayoutRes = nextStep;
showRelevantView();
}
}
@@ -203,12 +206,12 @@ public class SwapWorkflowActivity extends AppCompatActivity {
((FDroidApp) getApplication()).setSecureWindow(this);
super.onCreate(savedInstanceState);
- // The server should not be doing anything or occupying any (noticeable) resources
- // until we actually ask it to enable swapping. Therefore, we will start it nice and
- // early so we don't have to wait until it is connected later.
- Intent service = new Intent(this, SwapService.class);
- if (bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)) {
- startService(service);
+ currentView = new SwapView(this); // dummy placeholder to avoid NullPointerExceptions;
+
+ if (!bindService(new Intent(this, SwapService.class), serviceConnection,
+ BIND_ABOVE_CLIENT | BIND_IMPORTANT)) {
+ Toast.makeText(this, "ERROR: cannot bind to SwapService!", Toast.LENGTH_LONG).show();
+ finish();
}
setContentView(R.layout.swap_activity);
@@ -220,13 +223,19 @@ public class SwapWorkflowActivity extends AppCompatActivity {
container = (ViewGroup) findViewById(R.id.container);
localBroadcastManager = LocalBroadcastManager.getInstance(this);
+ localBroadcastManager.registerReceiver(downloaderInterruptedReceiver,
+ new IntentFilter(Downloader.ACTION_INTERRUPTED));
+
wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
new SwapDebug().logStatus();
}
@Override
protected void onDestroy() {
+ localBroadcastManager.unregisterReceiver(downloaderInterruptedReceiver);
unbindService(serviceConnection);
super.onDestroy();
}
@@ -333,8 +342,49 @@ public class SwapWorkflowActivity extends AppCompatActivity {
protected void onResume() {
super.onResume();
+ localBroadcastManager.registerReceiver(onWifiStateChanged,
+ new IntentFilter(WifiStateChangeService.BROADCAST));
+ localBroadcastManager.registerReceiver(localRepoStatus, new IntentFilter(LocalRepoService.ACTION_STATUS));
+ localBroadcastManager.registerReceiver(repoUpdateReceiver,
+ new IntentFilter(UpdateService.LOCAL_ACTION_STATUS));
+ localBroadcastManager.registerReceiver(bonjourFound, new IntentFilter(BonjourManager.ACTION_FOUND));
+ localBroadcastManager.registerReceiver(bonjourRemoved, new IntentFilter(BonjourManager.ACTION_REMOVED));
+ localBroadcastManager.registerReceiver(bonjourStatus, new IntentFilter(BonjourManager.ACTION_STATUS));
+ localBroadcastManager.registerReceiver(bluetoothFound, new IntentFilter(BluetoothManager.ACTION_FOUND));
+ localBroadcastManager.registerReceiver(bluetoothStatus, new IntentFilter(BluetoothManager.ACTION_STATUS));
+
+ registerReceiver(bluetoothScanModeChanged,
+ new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
+
checkIncomingIntent();
- showRelevantView();
+
+ if (newIntent) {
+ showRelevantView();
+ newIntent = false;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ unregisterReceiver(bluetoothScanModeChanged);
+
+ localBroadcastManager.unregisterReceiver(onWifiStateChanged);
+ localBroadcastManager.unregisterReceiver(localRepoStatus);
+ localBroadcastManager.unregisterReceiver(repoUpdateReceiver);
+ localBroadcastManager.unregisterReceiver(bonjourFound);
+ localBroadcastManager.unregisterReceiver(bonjourRemoved);
+ localBroadcastManager.unregisterReceiver(bonjourStatus);
+ localBroadcastManager.unregisterReceiver(bluetoothFound);
+ localBroadcastManager.unregisterReceiver(bluetoothStatus);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ newIntent = true;
}
/**
@@ -344,33 +394,19 @@ public class SwapWorkflowActivity extends AppCompatActivity {
*/
private void checkIncomingIntent() {
Intent intent = getIntent();
+ if (!ACTION_REQUEST_SWAP.equals(intent.getAction())) {
+ return;
+ }
Uri uri = intent.getData();
if (uri != null && !HttpDownloader.isSwapUrl(uri) && !BluetoothDownloader.isBluetoothUri(uri)) {
String msg = getString(R.string.swap_toast_invalid_url, uri);
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
return;
}
-
- if (intent.getBooleanExtra(EXTRA_CONFIRM, false) && !intent.getBooleanExtra(EXTRA_SWAP_INTENT_HANDLED, false)) {
- // Storing config in this variable will ensure that when showRelevantView() is next
- // run, it will show the connect swap view (if the service is available).
- intent.putExtra(EXTRA_SWAP_INTENT_HANDLED, true);
- confirmSwapConfig = new NewRepoConfig(this, intent);
- }
+ confirmSwapConfig = new NewRepoConfig(this, intent);
}
public void promptToSelectWifiNetwork() {
- //
- // On Android >= 5.0, the neutral button is the one by itself off to the left of a dialog
- // (not the negative button). Thus, the layout of this dialogs buttons should be:
- //
- // | |
- // +---------------------------------+
- // | Cancel Hotspot WiFi |
- // +---------------------------------+
- //
- // TODO: Investigate if this should be set dynamically for earlier APIs.
- //
new AlertDialog.Builder(this)
.setTitle(R.string.swap_join_same_wifi)
.setMessage(R.string.swap_join_same_wifi_desc)
@@ -417,33 +453,16 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
private void showRelevantView() {
- showRelevantView(false);
- }
- private void showRelevantView(boolean forceReload) {
-
- if (service == null) {
- inflateSwapView(R.layout.swap_initial_loading);
- return;
- }
-
- // This is separate from the switch statement below, because it is usually populated
- // during onResume, when there is a high probability of not having a swap service
- // available. Thus, we were unable to set the state of the swap service appropriately.
if (confirmSwapConfig != null) {
- showConfirmSwap(confirmSwapConfig);
+ inflateSwapView(R.layout.swap_confirm_receive);
+ setUpConfirmReceive();
confirmSwapConfig = null;
return;
}
- if (!forceReload && (container.getVisibility() == View.GONE || currentView != null && currentView.getLayoutResId() == service.getCurrentView())) {
- // Already showing the correct step, so don't bother changing anything.
- return;
- }
-
- int currentView = service.getCurrentView();
- switch (currentView) {
- case SwapService.STEP_INTRO:
+ switch (currentSwapViewLayoutRes) {
+ case STEP_INTRO:
showIntro();
return;
case R.layout.swap_nfc:
@@ -457,27 +476,17 @@ public class SwapWorkflowActivity extends AppCompatActivity {
inflateSwapView(R.layout.swap_start_swap);
return;
}
- inflateSwapView(currentView);
+ inflateSwapView(currentSwapViewLayoutRes);
}
- public SwapService getState() {
- return service;
- }
+ public void inflateSwapView(@LayoutRes int viewRes) {
+ getSwapService().initTimer();
- public SwapView inflateSwapView(@LayoutRes int viewRes) {
container.removeAllViews();
View view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false);
currentView = (SwapView) view;
currentView.setLayoutResId(viewRes);
-
- // Don't actually set the step to STEP_INITIAL_LOADING, as we are going to use this view
- // purely as a placeholder for _whatever view is meant to be shown_.
- if (currentView.getLayoutResId() != R.layout.swap_initial_loading) {
- if (service == null) {
- throw new IllegalStateException("We are not in the STEP_INITIAL_LOADING state, but the service is not ready.");
- }
- service.setCurrentView(currentView.getLayoutResId());
- }
+ currentSwapViewLayoutRes = viewRes;
toolbar.setBackgroundColor(currentView.getToolbarColour());
toolbar.setTitle(currentView.getToolbarTitle());
@@ -491,7 +500,28 @@ public class SwapWorkflowActivity extends AppCompatActivity {
container.addView(view);
supportInvalidateOptionsMenu();
- return currentView;
+ switch (currentView.getLayoutResId()) {
+ case R.layout.swap_send_fdroid:
+ setUpFromWifi();
+ setUpUseBluetoothButton();
+ break;
+ case R.layout.swap_wifi_qr:
+ setUpFromWifi();
+ setUpQrScannerButton();
+ break;
+ case R.layout.swap_nfc:
+ setUpNfcView();
+ break;
+ case R.layout.swap_select_apps:
+ LocalRepoService.create(this, getSwapService().getAppsToSwap());
+ break;
+ case R.layout.swap_connecting:
+ setUpConnectingView();
+ break;
+ case R.layout.swap_start_swap:
+ setUpStartVisibility();
+ break;
+ }
}
private void onToolbarCancel() {
@@ -502,42 +532,13 @@ public class SwapWorkflowActivity extends AppCompatActivity {
public void showIntro() {
// If we were previously swapping with a specific client, forget that we were doing that,
// as we are starting over now.
- getService().swapWith(null);
+ getSwapService().swapWith(null);
- if (!getService().isEnabled()) {
- if (!LocalRepoManager.get(this).getIndexJar().exists()) {
- Utils.debugLog(TAG, "Preparing initial repo with only F-Droid, until we have allowed the user to configure their own repo.");
- new PrepareInitialSwapRepo().execute();
- }
- }
+ LocalRepoService.create(this);
inflateSwapView(R.layout.swap_start_swap);
}
- private void showConfirmSwap(@NonNull NewRepoConfig config) {
- ((ConfirmReceiveView) inflateSwapView(R.layout.swap_confirm_receive)).setup(config);
- TextView descriptionTextView = (TextView) findViewById(R.id.text_description);
- descriptionTextView.setText(getResources().getString(R.string.swap_confirm_connect, config.getHost()));
- }
-
- public void startQrWorkflow() {
- if (!getService().isEnabled()) {
- new AlertDialog.Builder(this)
- .setTitle(R.string.not_visible_nearby)
- .setMessage(R.string.not_visible_nearby_description)
- .setCancelable(true)
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // Do nothing. The dialog will get dismissed anyway, which is all we ever wanted...
- }
- })
- .create().show();
- } else {
- inflateSwapView(R.layout.swap_wifi_qr);
- }
- }
-
/**
* On {@code android-26}, only apps with privileges can access
* {@code WRITE_SETTINGS}. So this just shows the tethering settings
@@ -562,10 +563,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
public void sendFDroid() {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- if (adapter == null
+ if (bluetoothAdapter == null
|| Build.VERSION.SDK_INT >= 23 // TODO make Bluetooth work with content:// URIs
- || (!adapter.isEnabled() && getService().getWifiSwap().isConnected())) {
+ || (!bluetoothAdapter.isEnabled() && LocalHTTPDManager.isAlive())) {
inflateSwapView(R.layout.swap_send_fdroid);
} else {
sendFDroidBluetooth();
@@ -578,7 +578,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
* automatically enable Bluetooth.
*/
public void sendFDroidBluetooth() {
- if (BluetoothAdapter.getDefaultAdapter().isEnabled()) {
+ if (bluetoothAdapter.isEnabled()) {
sendFDroidApk();
} else {
Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
@@ -591,32 +591,36 @@ public class SwapWorkflowActivity extends AppCompatActivity {
((FDroidApp) getApplication()).sendViaBluetooth(this, Activity.RESULT_OK, BuildConfig.APPLICATION_ID);
}
- // TODO: Figure out whether they have changed since last time UpdateAsyncTask was run.
- // If the local repo is running, then we can ask it what apps it is swapping and compare with that.
- // Otherwise, probably will need to scan the file system.
+ /**
+ * TODO: Figure out whether they have changed since last time LocalRepoService
+ * was run. If the local repo is running, then we can ask it what apps it is
+ * swapping and compare with that. Otherwise, probably will need to scan the
+ * file system.
+ */
public void onAppsSelected() {
- if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) {
- updateSwappableAppsTask = new PrepareSwapRepo(getService().getAppsToSwap());
- updateSwappableAppsTask.execute();
- getService().setCurrentView(R.layout.swap_connecting);
- inflateSwapView(R.layout.swap_connecting);
- } else {
+ if (hasPreparedLocalRepo) {
onLocalRepoPrepared();
+ } else {
+ LocalRepoService.create(this, getSwapService().getAppsToSwap());
+ currentSwapViewLayoutRes = R.layout.swap_connecting;
+ inflateSwapView(R.layout.swap_connecting);
}
}
/**
- * Once the UpdateAsyncTask has finished preparing our repository index, we can
+ * Once the LocalRepoService has finished preparing our repository index, we can
* show the next screen to the user. This will be one of two things:
- * * If we directly selected a peer to swap with initially, we will skip straight to getting
- * the list of apps from that device.
- * * Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code",
- * then we want to show a QR code or NFC dialog.
+ *
+ *
If we directly selected a peer to swap with initially, we will skip straight to getting
+ * the list of apps from that device.
+ *
Alternatively, if we didn't have a person to connect to, and instead clicked "Scan QR Code",
+ * then we want to show a QR code or NFC dialog.
+ *
*/
public void onLocalRepoPrepared() {
- updateSwappableAppsTask = null;
+ // TODO ditch this, use a message from LocalRepoService. Maybe?
hasPreparedLocalRepo = true;
- if (getService().isConnectingWithPeer()) {
+ if (getSwapService().isConnectingWithPeer()) {
startSwappingWithPeer();
} else if (!attemptToShowNfc()) {
inflateSwapView(R.layout.swap_wifi_qr);
@@ -624,7 +628,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
private void startSwappingWithPeer() {
- getService().connectToPeer();
+ getSwapService().connectToPeer();
inflateSwapView(R.layout.swap_connecting);
}
@@ -637,6 +641,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
// during the wifi qr code being shown too.
boolean nfcMessageReady = NfcHelper.setPushMessage(this, Utils.getSharingUri(FDroidApp.repo));
+ // TODO move all swap-specific preferences to a SharedPreferences instance for SwapWorkflowActivity
if (Preferences.get().showNfcDuringSwap() && nfcMessageReady) {
inflateSwapView(R.layout.swap_nfc);
return true;
@@ -645,7 +650,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
public void swapWith(Peer peer) {
- getService().swapWith(peer);
+ getSwapService().swapWith(peer);
inflateSwapView(R.layout.swap_select_apps);
}
@@ -659,13 +664,13 @@ public class SwapWorkflowActivity extends AppCompatActivity {
*/
public void swapWith(NewRepoConfig repoConfig) {
Peer peer = repoConfig.toPeer();
- if (getService().getCurrentView() == SwapService.STEP_INTRO || getService().getCurrentView() == R.layout.swap_confirm_receive) {
+ if (currentSwapViewLayoutRes == STEP_INTRO || currentSwapViewLayoutRes == R.layout.swap_confirm_receive) {
// This will force the "Select apps to swap" workflow to begin.
// TODO: Find a better way to decide whether we need to select the apps. Not sure if we
// can or cannot be in STEP_INTRO with a full blown repo ready to swap.
swapWith(peer);
} else {
- getService().swapWith(repoConfig.toPeer());
+ getSwapService().swapWith(peer);
startSwappingWithPeer();
}
}
@@ -696,8 +701,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
Toast.makeText(this, R.string.swap_qr_isnt_for_swap, Toast.LENGTH_SHORT).show();
}
}
- } else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
- finish();
} else if (requestCode == REQUEST_WRITE_SETTINGS_PERMISSION) {
if (Build.VERSION.SDK_INT >= 23 && Settings.System.canWrite(this)) {
setupWifiAP();
@@ -716,7 +719,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
if (resultCode != RESULT_CANCELED) {
Utils.debugLog(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server.");
- getState().getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService
+ BluetoothManager.start(this);
} else {
Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing");
SwapService.putBluetoothVisibleUserPreference(false);
@@ -740,12 +743,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
* involves pairing and connecting with other devices.
*/
public void startBluetoothSwap() {
-
- Utils.debugLog(TAG, "Initiating Bluetooth swap, will ensure the Bluetooth devices is enabled and discoverable before starting server.");
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-
- if (adapter != null) {
- if (adapter.isEnabled()) {
+ if (bluetoothAdapter != null) {
+ if (bluetoothAdapter.isEnabled()) {
Utils.debugLog(TAG, "Bluetooth enabled, will check if device is discoverable with device.");
ensureBluetoothDiscoverableThenStart();
} else {
@@ -758,104 +757,42 @@ public class SwapWorkflowActivity extends AppCompatActivity {
private void ensureBluetoothDiscoverableThenStart() {
Utils.debugLog(TAG, "Ensuring Bluetooth is in discoverable mode.");
- if (BluetoothAdapter.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-
- // TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery
- // is cancelled prematurely.
-
- // 3600 is new maximum! TODO: What about when this expires? What if user manually disables discovery?
- final int discoverableTimeout = 3600;
-
+ if (bluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Utils.debugLog(TAG, "Not currently in discoverable mode, so prompting user to enable.");
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
- intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, discoverableTimeout);
+ intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600); // 1 hour
startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE);
}
-
- if (service == null) {
- throw new IllegalStateException("Can't start Bluetooth swap because service is null for some strange reason.");
- }
-
- service.getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService
+ BluetoothManager.start(this);
}
- class PrepareInitialSwapRepo extends PrepareSwapRepo {
- PrepareInitialSwapRepo() {
- super(new HashSet<>(Arrays.asList(new String[]{BuildConfig.APPLICATION_ID})));
- }
- }
-
- class PrepareSwapRepo extends AsyncTask {
-
- public static final String ACTION = "PrepareSwapRepo.Action";
- public static final String EXTRA_MESSAGE = "PrepareSwapRepo.Status.Message";
- public static final String EXTRA_TYPE = "PrepareSwapRepo.Action.Type";
- public static final int TYPE_STATUS = 0;
- public static final int TYPE_COMPLETE = 1;
- public static final int TYPE_ERROR = 2;
-
- @NonNull
- protected final Set selectedApps;
-
- @NonNull
- protected final Uri sharingUri;
-
- @NonNull
- protected final Context context;
-
- PrepareSwapRepo(@NonNull Set apps) {
- context = SwapWorkflowActivity.this;
- selectedApps = apps;
- sharingUri = Utils.getSharingUri(FDroidApp.repo);
- }
-
- private void broadcast(int type) {
- broadcast(type, null);
- }
-
- private void broadcast(int type, String message) {
- Intent intent = new Intent(ACTION);
- intent.putExtra(EXTRA_TYPE, type);
- if (message != null) {
- Utils.debugLog(TAG, "Preparing swap: " + message);
- intent.putExtra(EXTRA_MESSAGE, message);
- }
- LocalBroadcastManager.getInstance(SwapWorkflowActivity.this).sendBroadcast(intent);
- }
-
+ private final BroadcastReceiver bluetoothScanModeChanged = new BroadcastReceiver() {
@Override
- protected Void doInBackground(Void... params) {
- try {
- final LocalRepoManager lrm = LocalRepoManager.get(context);
- broadcast(TYPE_STATUS, getString(R.string.deleting_repo));
- lrm.deleteRepo();
- for (String app : selectedApps) {
- broadcast(TYPE_STATUS, String.format(getString(R.string.adding_apks_format), app));
- lrm.addApp(context, app);
- }
- lrm.writeIndexPage(sharingUri.toString());
- broadcast(TYPE_STATUS, getString(R.string.writing_index_jar));
- lrm.writeIndexJar();
- broadcast(TYPE_STATUS, getString(R.string.linking_apks));
- lrm.copyApksToRepo();
- broadcast(TYPE_STATUS, getString(R.string.copying_icons));
- // run the icon copy without progress, its not a blocker
- new Thread() {
- @Override
- public void run() {
- android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
- lrm.copyIconsToRepo();
- }
- }.start();
-
- broadcast(TYPE_COMPLETE);
- } catch (Exception e) {
- broadcast(TYPE_ERROR);
- Log.e(TAG, "", e);
+ public void onReceive(Context context, Intent intent) {
+ SwitchCompat bluetoothSwitch = container.findViewById(R.id.switch_bluetooth);
+ TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible);
+ if (bluetoothSwitch == null || textBluetoothVisible == null
+ || !BluetoothManager.ACTION_STATUS.equals(intent.getAction())) {
+ return;
+ }
+ switch (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1)) {
+ case BluetoothAdapter.SCAN_MODE_NONE:
+ textBluetoothVisible.setText(R.string.disabled);
+ bluetoothSwitch.setEnabled(true);
+ break;
+
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+ textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
+ bluetoothSwitch.setEnabled(true);
+ break;
+
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+ textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
+ bluetoothSwitch.setEnabled(true);
+ break;
}
- return null;
}
- }
+ };
/**
* Helper class to try and make sense of what the swap workflow is currently doing.
@@ -882,26 +819,22 @@ public class SwapWorkflowActivity extends AppCompatActivity {
if (service == null) {
message = "No swap service";
} else {
- String bluetooth = service.getBluetoothSwap().isConnected() ? "Y" : " N";
- String wifi = service.getWifiSwap().isConnected() ? "Y" : " N";
- String mdns = service.getWifiSwap().getBonjour().isConnected() ? "Y" : " N";
- message += "Swap { BT: " + bluetooth + ", WiFi: " + wifi + ", mDNS: " + mdns + "}, ";
+ String bluetooth;
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
bluetooth = "N/A";
- if (adapter != null) {
+ if (bluetoothAdapter != null) {
Map scanModes = new HashMap<>(3);
scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE, "CON");
scanModes.put(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, "CON_DISC");
scanModes.put(BluetoothAdapter.SCAN_MODE_NONE, "NONE");
- bluetooth = "\"" + adapter.getName() + "\" - " + scanModes.get(adapter.getScanMode());
+ bluetooth = "\"" + bluetoothAdapter.getName() + "\" - "
+ + scanModes.get(bluetoothAdapter.getScanMode());
}
-
- message += "Find { BT: " + bluetooth + ", WiFi: " + wifi + "}";
}
Date now = new Date();
- Utils.debugLog("Swap Status", now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() + " " + message);
+ Utils.debugLog("SWAP_STATUS",
+ now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds() + " " + message);
new Timer().schedule(new TimerTask() {
@Override
@@ -913,42 +846,522 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
}
- public void install(@NonNull final App app, @NonNull final Apk apk) {
- localBroadcastManager.registerReceiver(installReceiver,
- Installer.getInstallIntentFilter(apk.getCanonicalUrl()));
- InstallManagerService.queue(this, app, apk);
- }
-
- private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver onWifiStateChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- switch (intent.getAction()) {
- case Installer.ACTION_INSTALL_STARTED:
- break;
- case Installer.ACTION_INSTALL_COMPLETE:
- localBroadcastManager.unregisterReceiver(this);
+ setUpFromWifi();
- showRelevantView(true);
+ int wifiStatus = -1;
+ TextView textWifiVisible = container.findViewById(R.id.wifi_visible);
+ if (textWifiVisible != null) {
+ intent.getIntExtra(WifiStateChangeService.EXTRA_STATUS, -1);
+ }
+ switch (wifiStatus) {
+ case WifiManager.WIFI_STATE_ENABLING:
+ textWifiVisible.setText(R.string.swap_setting_up_wifi);
break;
- case Installer.ACTION_INSTALL_INTERRUPTED:
- localBroadcastManager.unregisterReceiver(this);
- // TODO: handle errors!
+ case WifiManager.WIFI_STATE_ENABLED:
+ textWifiVisible.setText(R.string.swap_not_visible_wifi);
break;
- case Installer.ACTION_INSTALL_USER_INTERACTION:
- PendingIntent installPendingIntent =
- intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
-
- try {
- installPendingIntent.send();
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "PI canceled", e);
- }
-
+ case WifiManager.WIFI_STATE_DISABLING:
+ case WifiManager.WIFI_STATE_DISABLED:
+ textWifiVisible.setText(R.string.swap_stopping_wifi);
+ break;
+ case WifiManager.WIFI_STATE_UNKNOWN:
break;
- default:
- throw new RuntimeException("intent action not handled!");
}
}
};
+ private void setUpFromWifi() {
+ String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://";
+
+ // the fingerprint is not useful on the button label
+ String buttonLabel = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port;
+ TextView ipAddressView = container.findViewById(R.id.device_ip_address);
+ if (ipAddressView != null) {
+ ipAddressView.setText(buttonLabel);
+ }
+
+ String qrUriString = null;
+ switch (currentView.getLayoutResId()) {
+ case R.layout.swap_join_wifi:
+ setUpJoinWifi();
+ return;
+ case R.layout.swap_send_fdroid:
+ qrUriString = buttonLabel;
+ break;
+ case R.layout.swap_wifi_qr:
+ Uri sharingUri = Utils.getSharingUri(FDroidApp.repo);
+ StringBuilder qrUrlBuilder = new StringBuilder(scheme);
+ qrUrlBuilder.append(sharingUri.getHost());
+ if (sharingUri.getPort() != 80) {
+ qrUrlBuilder.append(':');
+ qrUrlBuilder.append(sharingUri.getPort());
+ }
+ qrUrlBuilder.append(sharingUri.getPath());
+ boolean first = true;
+
+ Set names = sharingUri.getQueryParameterNames();
+ for (String name : names) {
+ if (!"ssid".equals(name)) {
+ if (first) {
+ qrUrlBuilder.append('?');
+ first = false;
+ } else {
+ qrUrlBuilder.append('&');
+ }
+ qrUrlBuilder.append(name.toUpperCase(Locale.ENGLISH));
+ qrUrlBuilder.append('=');
+ qrUrlBuilder.append(sharingUri.getQueryParameter(name).toUpperCase(Locale.ENGLISH));
+ }
+ }
+ qrUriString = qrUrlBuilder.toString();
+ break;
+ }
+
+ ImageView qrImage = container.findViewById(R.id.wifi_qr_code);
+ if (qrUriString != null && qrImage != null) {
+ Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString);
+ new QrGenAsyncTask(SwapWorkflowActivity.this, R.id.wifi_qr_code).execute(qrUriString);
+
+ // Replace all blacks with the background blue.
+ qrImage.setColorFilter(new LightingColorFilter(0xffffffff, getResources().getColor(R.color.swap_blue)));
+
+ final View qrWarningMessage = container.findViewById(R.id.warning_qr_scanner);
+ if (CameraCharacteristicsChecker.getInstance(this).hasAutofocus()) {
+ qrWarningMessage.setVisibility(View.GONE);
+ } else {
+ qrWarningMessage.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ // TODO: Listen for "Connecting..." state and reflect that in the view too.
+ private void setUpJoinWifi() {
+ currentView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK));
+ }
+ });
+ TextView descriptionView = container.findViewById(R.id.text_description);
+ ImageView wifiIcon = container.findViewById(R.id.wifi_icon);
+ TextView ssidView = container.findViewById(R.id.wifi_ssid);
+ TextView tapView = container.findViewById(R.id.wifi_available_networks_prompt);
+ if (TextUtils.isEmpty(FDroidApp.bssid) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) {
+ // empty bssid with an ipAddress means hotspot mode
+ descriptionView.setText(R.string.swap_join_this_hotspot);
+ wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.hotspot));
+ ssidView.setText(R.string.swap_active_hotspot);
+ tapView.setText(R.string.swap_switch_to_wifi);
+ } else if (TextUtils.isEmpty(FDroidApp.ssid)) {
+ // not connected to or setup with any wifi network
+ descriptionView.setText(R.string.swap_join_same_wifi);
+ wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.wifi));
+ ssidView.setText(R.string.swap_no_wifi_network);
+ tapView.setText(R.string.swap_view_available_networks);
+ } else {
+ // connected to a regular wifi network
+ descriptionView.setText(R.string.swap_join_same_wifi);
+ wifiIcon.setImageDrawable(getResources().getDrawable(R.drawable.wifi));
+ ssidView.setText(FDroidApp.ssid);
+ tapView.setText(R.string.swap_view_available_networks);
+ }
+ }
+
+ private void setUpStartVisibility() {
+ TextView viewWifiNetwork = findViewById(R.id.wifi_network);
+
+ viewWifiNetwork.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ promptToSelectWifiNetwork();
+ }
+ });
+
+ SwitchCompat wifiSwitch = findViewById(R.id.switch_wifi);
+ wifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Context context = getApplicationContext();
+ if (isChecked) {
+ wifiManager.setWifiEnabled(true);
+ BonjourManager.start(context);
+ }
+ BonjourManager.setVisible(context, isChecked);
+ SwapService.putWifiVisibleUserPreference(isChecked);
+ }
+ });
+
+ findViewById(R.id.btn_scan_qr).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ inflateSwapView(R.layout.swap_wifi_qr);
+ }
+ });
+
+ if (SwapService.getWifiVisibleUserPreference()) {
+ wifiSwitch.setChecked(true);
+ } else {
+ wifiSwitch.setChecked(false);
+ }
+ }
+
+ private final BroadcastReceiver bonjourStatus = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ TextView textWifiVisible = container.findViewById(R.id.wifi_visible);
+ TextView peopleNearbyText = container.findViewById(R.id.text_people_nearby);
+ ProgressBar peopleNearbyProgress = container.findViewById(R.id.searching_people_nearby);
+ if (textWifiVisible == null || peopleNearbyText == null || peopleNearbyProgress == null
+ || !BonjourManager.ACTION_STATUS.equals(intent.getAction())) {
+ return;
+ }
+ int status = intent.getIntExtra(BonjourManager.EXTRA_STATUS, -1);
+ Log.i(TAG, "BonjourManager.EXTRA_STATUS: " + status);
+ switch (status) {
+ case BonjourManager.STATUS_STARTING:
+ textWifiVisible.setText(R.string.swap_setting_up_wifi);
+ peopleNearbyText.setText(R.string.swap_starting);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BonjourManager.STATUS_STARTED:
+ textWifiVisible.setText(R.string.swap_not_visible_wifi);
+ peopleNearbyText.setText(R.string.swap_scanning_for_peers);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BonjourManager.STATUS_NOT_VISIBLE:
+ textWifiVisible.setText(R.string.swap_not_visible_wifi);
+ peopleNearbyText.setText(R.string.swap_scanning_for_peers);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BonjourManager.STATUS_VISIBLE:
+ textWifiVisible.setText(R.string.swap_visible_wifi);
+ peopleNearbyText.setText(R.string.swap_scanning_for_peers);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BonjourManager.STATUS_STOPPING:
+ textWifiVisible.setText(R.string.swap_stopping_wifi);
+ if (!BluetoothManager.isAlive()) {
+ peopleNearbyText.setText(R.string.swap_stopping);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ }
+ break;
+ case BonjourManager.STATUS_STOPPED:
+ textWifiVisible.setText(R.string.swap_not_visible_wifi);
+ if (!BluetoothManager.isAlive()) {
+ peopleNearbyText.setVisibility(View.GONE);
+ peopleNearbyProgress.setVisibility(View.GONE);
+ }
+ break;
+ case BonjourManager.STATUS_ERROR:
+ textWifiVisible.setText(R.string.swap_not_visible_wifi);
+ peopleNearbyText.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.GONE);
+ default:
+ throw new IllegalArgumentException("Bad intent: " + intent);
+ }
+ }
+ };
+
+ private final BroadcastReceiver bonjourFound = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby);
+ if (peopleNearbyList != null) {
+ ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter();
+ peopleNearbyAdapter.add((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
+ }
+ }
+ };
+
+ private final BroadcastReceiver bonjourRemoved = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby);
+ if (peopleNearbyList != null) {
+ ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter();
+ peopleNearbyAdapter.remove((Peer) intent.getParcelableExtra(BonjourManager.EXTRA_BONJOUR_PEER));
+ }
+ }
+ };
+
+ private final BroadcastReceiver bluetoothStatus = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ SwitchCompat bluetoothSwitch = container.findViewById(R.id.switch_bluetooth);
+ TextView textBluetoothVisible = container.findViewById(R.id.bluetooth_visible);
+ TextView textDeviceIdBluetooth = container.findViewById(R.id.device_id_bluetooth);
+ TextView peopleNearbyText = container.findViewById(R.id.text_people_nearby);
+ ProgressBar peopleNearbyProgress = container.findViewById(R.id.searching_people_nearby);
+ if (bluetoothSwitch == null || textBluetoothVisible == null || textDeviceIdBluetooth == null
+ || peopleNearbyText == null || peopleNearbyProgress == null
+ || !BluetoothManager.ACTION_STATUS.equals(intent.getAction())) {
+ return;
+ }
+ int status = intent.getIntExtra(BluetoothManager.EXTRA_STATUS, -1);
+ Log.i(TAG, "BluetoothManager.EXTRA_STATUS: " + status);
+ switch (status) {
+ case BluetoothManager.STATUS_STARTING:
+ bluetoothSwitch.setEnabled(false);
+ textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth);
+ textDeviceIdBluetooth.setVisibility(View.VISIBLE);
+ peopleNearbyText.setText(R.string.swap_scanning_for_peers);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BluetoothManager.STATUS_STARTED:
+ bluetoothSwitch.setEnabled(true);
+ textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
+ textDeviceIdBluetooth.setVisibility(View.VISIBLE);
+ peopleNearbyText.setText(R.string.swap_scanning_for_peers);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ break;
+ case BluetoothManager.STATUS_STOPPING:
+ bluetoothSwitch.setEnabled(false);
+ textBluetoothVisible.setText(R.string.swap_stopping);
+ textDeviceIdBluetooth.setVisibility(View.GONE);
+ if (!BonjourManager.isAlive()) {
+ peopleNearbyText.setText(R.string.swap_stopping);
+ peopleNearbyText.setVisibility(View.VISIBLE);
+ peopleNearbyProgress.setVisibility(View.VISIBLE);
+ }
+ break;
+ case BluetoothManager.STATUS_STOPPED:
+ bluetoothSwitch.setEnabled(true);
+ textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
+ textDeviceIdBluetooth.setVisibility(View.GONE);
+ if (!BonjourManager.isAlive()) {
+ peopleNearbyText.setVisibility(View.GONE);
+ peopleNearbyProgress.setVisibility(View.GONE);
+ }
+
+ ListView peopleNearbyView = container.findViewById(R.id.list_people_nearby);
+ if (peopleNearbyView == null) {
+ break;
+ }
+ ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyView.getAdapter();
+ for (int i = 0; i < peopleNearbyAdapter.getCount(); i++) {
+ Peer peer = (Peer) peopleNearbyAdapter.getItem(i);
+ if (peer.getClass().equals(BluetoothPeer.class)) {
+ Utils.debugLog(TAG, "Removing bluetooth peer: " + peer.getName());
+ peopleNearbyAdapter.remove(peer);
+ }
+ }
+ break;
+ case BluetoothManager.STATUS_ERROR:
+ bluetoothSwitch.setEnabled(true);
+ textBluetoothVisible.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
+ textDeviceIdBluetooth.setVisibility(View.VISIBLE);
+ break;
+ default:
+ throw new IllegalArgumentException("Bad intent: " + intent);
+ }
+ }
+ };
+
+ private final BroadcastReceiver bluetoothFound = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ListView peopleNearbyList = container.findViewById(R.id.list_people_nearby);
+ if (peopleNearbyList != null) {
+ ArrayAdapter peopleNearbyAdapter = (ArrayAdapter) peopleNearbyList.getAdapter();
+ peopleNearbyAdapter.add((Peer) intent.getParcelableExtra(BluetoothManager.EXTRA_PEER));
+ }
+ }
+ };
+
+ private void setUpUseBluetoothButton() {
+ Button useBluetooth = findViewById(R.id.btn_use_bluetooth);
+ if (useBluetooth != null) {
+ useBluetooth.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showIntro();
+ sendFDroidBluetooth();
+ }
+ });
+ }
+ }
+
+ private void setUpQrScannerButton() {
+ Button openQr = findViewById(R.id.btn_qr_scanner);
+ if (openQr != null) {
+ openQr.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ initiateQrScan();
+ }
+ });
+ }
+ }
+
+ private void setUpConfirmReceive() {
+ TextView descriptionTextView = findViewById(R.id.text_description);
+ if (descriptionTextView != null) {
+ descriptionTextView.setText(getString(R.string.swap_confirm_connect, confirmSwapConfig.getHost()));
+ }
+
+ Button confirmReceiveYes = container.findViewById(R.id.confirm_receive_yes);
+ if (confirmReceiveYes != null) {
+ findViewById(R.id.confirm_receive_yes).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ denySwap();
+ }
+ });
+ }
+
+ Button confirmReceiveNo = container.findViewById(R.id.confirm_receive_no);
+ if (confirmReceiveNo != null) {
+ findViewById(R.id.confirm_receive_no).setOnClickListener(new View.OnClickListener() {
+
+ private final NewRepoConfig config = confirmSwapConfig;
+
+ @Override
+ public void onClick(View v) {
+ swapWith(config);
+ }
+ });
+ }
+ }
+
+ private void setUpNfcView() {
+ CheckBox dontShowAgain = container.findViewById(R.id.checkbox_dont_show);
+ dontShowAgain.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.get().setShowNfcDuringSwap(!isChecked);
+ }
+ });
+
+ }
+
+ private void setUpConnectingProgressText(String message) {
+ TextView progressText = container.findViewById(R.id.progress_text);
+ if (progressText != null && message != null) {
+ progressText.setVisibility(View.VISIBLE);
+ progressText.setText(message);
+ }
+ }
+
+ /**
+ * Listens for feedback about a local repository being prepared, like APK
+ * files copied to the LocalHTTPD webroot, the {@code index.html} generated,
+ * etc. Icons will be copied to the webroot in the background and so are
+ * not part of this process.
+ */
+ private final BroadcastReceiver localRepoStatus = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ setUpConnectingProgressText(intent.getStringExtra(Intent.EXTRA_TEXT));
+
+ ProgressBar progressBar = container.findViewById(R.id.progress_bar);
+ Button tryAgainButton = container.findViewById(R.id.try_again);
+
+ if (progressBar == null || tryAgainButton == null) {
+ Utils.debugLog(TAG, "prepareSwapReceiver received intent without view: " + intent);
+ return;
+ }
+
+ switch (intent.getIntExtra(LocalRepoService.EXTRA_STATUS, -1)) {
+ case LocalRepoService.STATUS_PROGRESS:
+ progressBar.setVisibility(View.VISIBLE);
+ tryAgainButton.setVisibility(View.GONE);
+ break;
+ case LocalRepoService.STATUS_STARTED:
+ progressBar.setVisibility(View.VISIBLE);
+ tryAgainButton.setVisibility(View.GONE);
+ onLocalRepoPrepared();
+ break;
+ case LocalRepoService.STATUS_ERROR:
+ progressBar.setVisibility(View.GONE);
+ tryAgainButton.setVisibility(View.VISIBLE);
+ break;
+ default:
+ throw new IllegalArgumentException("Bogus intent: " + intent);
+ }
+ }
+ };
+
+ /**
+ * Listens for feedback about a repo update process taking place.
+ * Tracks an index.jar download and show the progress messages
+ */
+ private final BroadcastReceiver repoUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String message = intent.getStringExtra(UpdateService.EXTRA_MESSAGE);
+ if (message == null) {
+ CharSequence[] repoErrors = intent.getCharSequenceArrayExtra(UpdateService.EXTRA_REPO_ERRORS);
+ if (repoErrors != null) {
+ StringBuilder msgBuilder = new StringBuilder();
+ for (CharSequence error : repoErrors) {
+ if (msgBuilder.length() > 0) {
+ msgBuilder.append(" + ");
+ }
+ msgBuilder.append(error);
+ }
+ message = msgBuilder.toString();
+ }
+ }
+ setUpConnectingProgressText(message);
+
+ ProgressBar progressBar = container.findViewById(R.id.progress_bar);
+ Button tryAgainButton = container.findViewById(R.id.try_again);
+
+ if (progressBar == null || tryAgainButton == null) {
+ Utils.debugLog(TAG, "repoUpdateReceiver received intent without view: " + intent);
+ return;
+ }
+
+ int status = intent.getIntExtra(UpdateService.EXTRA_STATUS_CODE, -1);
+ if (status == UpdateService.STATUS_ERROR_GLOBAL ||
+ status == UpdateService.STATUS_ERROR_LOCAL ||
+ status == UpdateService.STATUS_ERROR_LOCAL_SMALL) {
+ progressBar.setVisibility(View.GONE);
+ tryAgainButton.setVisibility(View.VISIBLE);
+ getSwapService().removeCurrentPeerFromActive();
+ return;
+ } else {
+ progressBar.setVisibility(View.VISIBLE);
+ tryAgainButton.setVisibility(View.GONE);
+ getSwapService().addCurrentPeerToActive();
+ }
+
+ if (status == UpdateService.STATUS_COMPLETE_AND_SAME
+ || status == UpdateService.STATUS_COMPLETE_WITH_CHANGES) {
+ inflateSwapView(R.layout.swap_success);
+ }
+ }
+ };
+
+ private final BroadcastReceiver downloaderInterruptedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Repo repo = RepoProvider.Helper.findByUrl(context, intent.getData(), null);
+ if (repo != null && repo.isSwap) {
+ setUpConnectingProgressText(intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE));
+ }
+ }
+ };
+
+ private void setUpConnectingView() {
+ TextView heading = container.findViewById(R.id.progress_text);
+ heading.setText(R.string.swap_connecting);
+ container.findViewById(R.id.try_again).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onAppsSelected();
+ }
+ });
+ }
}
diff --git a/app/src/full/java/org/fdroid/fdroid/views/swap/WifiQrView.java b/app/src/full/java/org/fdroid/fdroid/views/swap/WifiQrView.java
deleted file mode 100644
index ad8fadbba..000000000
--- a/app/src/full/java/org/fdroid/fdroid/views/swap/WifiQrView.java
+++ /dev/null
@@ -1,143 +0,0 @@
-package org.fdroid.fdroid.views.swap;
-
-import android.annotation.TargetApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.graphics.LightingColorFilter;
-import android.net.Uri;
-import android.support.v4.content.LocalBroadcastManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Preferences;
-import org.fdroid.fdroid.QrGenAsyncTask;
-import org.fdroid.fdroid.R;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.SwapView;
-import org.fdroid.fdroid.net.WifiStateChangeService;
-import org.fdroid.fdroid.views.swap.device.camera.CameraCharacteristicsChecker;
-
-import java.util.Locale;
-import java.util.Set;
-
-public class WifiQrView extends SwapView {
-
- private static final String TAG = "WifiQrView";
-
- public WifiQrView(Context context) {
- super(context);
- }
-
- public WifiQrView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public WifiQrView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @TargetApi(21)
- public WifiQrView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- setUIFromWifi();
- setUpWarningMessageQrScan();
-
- ImageView qrImage = (ImageView) findViewById(R.id.wifi_qr_code);
-
- // Replace all blacks with the background blue.
- qrImage.setColorFilter(new LightingColorFilter(0xffffffff, getResources().getColor(R.color.swap_blue)));
-
- Button openQr = (Button) findViewById(R.id.btn_qr_scanner);
- openQr.setOnClickListener(new Button.OnClickListener() {
- @Override
- public void onClick(View v) {
- getActivity().initiateQrScan();
- }
- });
-
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- onWifiStateChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
- }
-
- private void setUpWarningMessageQrScan() {
- final View qrWarnningMessage = findViewById(R.id.warning_qr_scanner);
- final boolean hasAutofocus = CameraCharacteristicsChecker.getInstance(getContext()).hasAutofocus();
- final int visiblity = hasAutofocus ? GONE : VISIBLE;
- qrWarnningMessage.setVisibility(visiblity);
- }
-
-
- /**
- * Remove relevant listeners/receivers/etc so that they do not receive and process events
- * when this view is not in use.
- */
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(onWifiStateChanged);
- }
-
- private void setUIFromWifi() {
-
- if (TextUtils.isEmpty(FDroidApp.repo.address)) {
- return;
- }
-
- String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://";
-
- // the fingerprint is not useful on the button label
- String buttonLabel = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port;
- TextView ipAddressView = (TextView) findViewById(R.id.device_ip_address);
- ipAddressView.setText(buttonLabel);
-
- Uri sharingUri = Utils.getSharingUri(FDroidApp.repo);
- StringBuilder qrUrlBuilder = new StringBuilder(scheme);
- qrUrlBuilder.append(sharingUri.getHost());
- if (sharingUri.getPort() != 80) {
- qrUrlBuilder.append(':');
- qrUrlBuilder.append(sharingUri.getPort());
- }
- qrUrlBuilder.append(sharingUri.getPath());
- boolean first = true;
-
- Set names = sharingUri.getQueryParameterNames();
- for (String name : names) {
- if (!"ssid".equals(name)) {
- if (first) {
- qrUrlBuilder.append('?');
- first = false;
- } else {
- qrUrlBuilder.append('&');
- }
- qrUrlBuilder.append(name.toUpperCase(Locale.ENGLISH));
- qrUrlBuilder.append('=');
- qrUrlBuilder.append(sharingUri.getQueryParameter(name).toUpperCase(Locale.ENGLISH));
- }
- }
-
- String qrUriString = qrUrlBuilder.toString();
- Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString);
- new QrGenAsyncTask(getActivity(), R.id.wifi_qr_code).execute(qrUriString);
-
- }
-
- private final BroadcastReceiver onWifiStateChanged = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- setUIFromWifi();
- }
- };
-
-}
diff --git a/app/src/full/res/layout/swap_app_list_item.xml b/app/src/full/res/layout/swap_app_list_item.xml
index cbe716ff7..0a8261475 100644
--- a/app/src/full/res/layout/swap_app_list_item.xml
+++ b/app/src/full/res/layout/swap_app_list_item.xml
@@ -82,14 +82,14 @@
android:layout_height="wrap_content"
android:indeterminate="true"
style="?android:attr/progressBarStyleHorizontal"
- android:layout_toEndOf="@android:id/icon"
android:paddingStart="5dp"
android:paddingLeft="5dp"
android:paddingEnd="5dp"
android:paddingRight="5dp"
+ android:layout_toEndOf="@android:id/icon"
android:layout_toRightOf="@android:id/icon"
- android:layout_alignParentRight="true"
- android:layout_alignParentEnd="true"
+ android:layout_toStartOf="@+id/button_or_text"
+ android:layout_toLeftOf="@+id/button_or_text"
android:layout_below="@+id/name"
/>
diff --git a/app/src/full/res/layout/swap_confirm_receive.xml b/app/src/full/res/layout/swap_confirm_receive.xml
index 93cb17a52..a65d43043 100644
--- a/app/src/full/res/layout/swap_confirm_receive.xml
+++ b/app/src/full/res/layout/swap_confirm_receive.xml
@@ -1,6 +1,6 @@
-
-
-
+
diff --git a/app/src/full/res/layout/swap_connecting.xml b/app/src/full/res/layout/swap_connecting.xml
index deb4b5c71..c4ecd31a6 100644
--- a/app/src/full/res/layout/swap_connecting.xml
+++ b/app/src/full/res/layout/swap_connecting.xml
@@ -1,6 +1,6 @@
-
-
-
+ android:layout_below="@+id/progress_text"/>
+ android:text="@string/try_again"/>
-
+
diff --git a/app/src/full/res/layout/swap_initial_loading.xml b/app/src/full/res/layout/swap_initial_loading.xml
deleted file mode 100644
index 502420b66..000000000
--- a/app/src/full/res/layout/swap_initial_loading.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/full/res/layout/swap_join_wifi.xml b/app/src/full/res/layout/swap_join_wifi.xml
index b1e1146cc..6cce7801b 100644
--- a/app/src/full/res/layout/swap_join_wifi.xml
+++ b/app/src/full/res/layout/swap_join_wifi.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/full/res/layout/swap_nfc.xml b/app/src/full/res/layout/swap_nfc.xml
index 01c9533be..454ceee21 100644
--- a/app/src/full/res/layout/swap_nfc.xml
+++ b/app/src/full/res/layout/swap_nfc.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/full/res/layout/swap_send_fdroid.xml b/app/src/full/res/layout/swap_send_fdroid.xml
index c86099843..d0a8fa371 100644
--- a/app/src/full/res/layout/swap_send_fdroid.xml
+++ b/app/src/full/res/layout/swap_send_fdroid.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/full/res/layout/swap_start_swap.xml b/app/src/full/res/layout/swap_start_swap.xml
index 7a2137e32..0d7e639ee 100644
--- a/app/src/full/res/layout/swap_start_swap.xml
+++ b/app/src/full/res/layout/swap_start_swap.xml
@@ -1,9 +1,4 @@
-
-
@@ -168,11 +164,12 @@
android:drawablePadding="10dp"
android:paddingLeft="25dp"
android:paddingRight="25dp"
+ android:paddingStart="25dp"
android:paddingEnd="25dp"
android:background="@android:color/transparent"/>
diff --git a/app/src/full/res/layout/swap_wifi_qr.xml b/app/src/full/res/layout/swap_wifi_qr.xml
index 6678e3b84..7e46c10d1 100644
--- a/app/src/full/res/layout/swap_wifi_qr.xml
+++ b/app/src/full/res/layout/swap_wifi_qr.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
index dfbfcac9a..9c45db4f9 100644
--- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java
+++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
@@ -487,7 +487,12 @@ public class UpdateService extends JobIntentService {
}
} catch (IndexUpdater.UpdateException e) {
errorRepos++;
- repoErrors.add(e.getLocalizedMessage());
+ Throwable cause = e.getCause();
+ if (cause == null) {
+ repoErrors.add(e.getLocalizedMessage());
+ } else {
+ repoErrors.add(e.getLocalizedMessage() + " ⇨ " + cause.getLocalizedMessage());
+ }
Log.e(TAG, "Error updating repository " + repo.address);
e.printStackTrace();
}
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/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index c7badbb29..c13b53990 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -385,13 +385,16 @@ public class App extends ValueObject implements Comparable, Parcelable {
* exists.
*/
@Nullable
- public static App getInstance(Context context, PackageManager pm, String packageName)
+ public static App getInstance(Context context, PackageManager pm, InstalledApp installedApp, String packageName)
throws CertificateEncodingException, IOException, PackageManager.NameNotFoundException {
App app = new App();
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
app.installedApk = new Apk();
- if (apkFile.canRead()) {
+ if (installedApp != null) {
+ app.installedApk.hashType = installedApp.getHashType();
+ app.installedApk.hash = installedApp.getHash();
+ } else if (apkFile.canRead()) {
String hashType = "sha256";
String hash = Utils.getBinaryHash(apkFile, hashType);
if (TextUtils.isEmpty(hash)) {
diff --git a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
index 56873ab76..8ea05577d 100644
--- a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
+++ b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothConnection.java
@@ -47,7 +47,7 @@ public class BluetoothConnection {
Utils.closeQuietly(socket);
}
- public void close() throws IOException {
+ public void close() {
closeQuietly();
}
}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java
index 77160ceb8..abd432b97 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java
@@ -25,7 +25,6 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -702,7 +701,7 @@ public class AppDetailsActivity extends AppCompatActivity
if (Build.VERSION.SDK_INT < 18) {
return BluetoothAdapter.getDefaultAdapter();
}
- return ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter();
+ return ((android.bluetooth.BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter();
}
@Override
diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
index 5e9daf06b..c04116e63 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainActivity.java
@@ -92,11 +92,10 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
+ public static final String ACTION_REQUEST_SWAP = "requestSwap";
private static final String STATE_SELECTED_MENU_ID = "selectedMenuId";
- private static final int REQUEST_SWAP = 3;
-
private RecyclerView pager;
private MainViewAdapter adapter;
private BottomNavigationBar bottomNavigation;
@@ -390,10 +389,7 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
NewRepoConfig parser = new NewRepoConfig(this, intent);
if (parser.isValidRepo()) {
if (parser.isFromSwap()) {
- Intent confirmIntent = new Intent(this, SwapWorkflowActivity.class);
- confirmIntent.putExtra(SwapWorkflowActivity.EXTRA_CONFIRM, true);
- confirmIntent.setData(intent.getData());
- startActivityForResult(confirmIntent, REQUEST_SWAP);
+ SwapWorkflowActivity.requestSwap(this, intent.getData());
} else {
Intent clean = new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class);
if (intent.hasExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO)) {
diff --git a/app/src/main/res/layout/app_details2_version_item.xml b/app/src/main/res/layout/app_details2_version_item.xml
index 194a08489..452f8cd64 100644
--- a/app/src/main/res/layout/app_details2_version_item.xml
+++ b/app/src/main/res/layout/app_details2_version_item.xml
@@ -1,6 +1,5 @@
IconNextSkip
+ Try againUse TorForce download traffic through Tor for increased privacy. Requires Orbot
@@ -455,6 +456,7 @@ This often occurs with apps installed via Google Play or other sources, if they
Try it
+
Touch to swapIf your friend has F-Droid and NFC turned on touch your devices together.
@@ -463,7 +465,9 @@ This often occurs with apps installed via Google Play or other sources, if they
access to the same network, one of you can create a Wi-Fi Hotspot.
Help your friend join your hotspot
+
Swap apps
+
Swap success!No network yet%1$s (your hotspot)
@@ -475,14 +479,21 @@ This often occurs with apps installed via Google Play or other sources, if they
Don\'t show this againOne person needs to scan the code, or type the URL of the other in a browser.
+
Choose Apps
+
Scan QR CodePeople NearbySearching for nearby people…
+
Nearby SwapConnect and trade apps with people near you.
+ Starting…
+ Stopping…
+ DisabledVisible via BluetoothSetting up Bluetooth…
+ Cannot start Bluetooth!Not visible via BluetoothVisible via Wi-FiSetting up Wi-Fi…
@@ -490,9 +501,12 @@ This often occurs with apps installed via Google Play or other sources, if they
Not visible via Wi-FiDevice NameCan\'t find who you\'re looking for?
+
Send F-DroidCould not find people nearby to swap with.
+
Connecting
+
Confirm swapThe QR code you scanned doesn\'t look like a swap code.Use Bluetooth
@@ -507,6 +521,7 @@ This often occurs with apps installed via Google Play or other sources, if they
Invalid URL for swapping: %1$sWi-Fi Hotspot enabledCould not enable Wi-Fi Hotspot!
+ Nearby closed since it was idle.needs access toDo you want to install an update
diff --git a/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
new file mode 100644
index 000000000..a9534989f
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
@@ -0,0 +1,14 @@
+package org.fdroid.fdroid.data;
+
+import android.content.Context;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(App.class)
+public class ShadowApp extends ValueObject {
+
+ @Implementation
+ protected static int[] getMinTargetMaxSdkVersions(Context context, String packageName) {
+ return new int[]{10, 23, Apk.SDK_VERSION_MAX_VALUE};
+ }
+}
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));
+ }
+}
diff --git a/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java b/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
index 1c3b8e0ea..7f6e11766 100644
--- a/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
+++ b/app/src/testFull/java/org/fdroid/fdroid/net/LocalHTTPDTest.java
@@ -37,6 +37,7 @@ import android.content.Context;
import android.text.TextUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
+import org.fdroid.fdroid.Utils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -76,11 +77,16 @@ public class LocalHTTPDTest {
private static Thread serverStartThread;
private static File webRoot;
+ private final int port = 38723;
+ private final String baseUrl = "http://localhost:" + port;
+
@Before
public void setUp() throws Exception {
ShadowLog.stream = System.out;
classLoader = getClass().getClassLoader();
+ assertFalse(Utils.isServerSocketInUse(port));
+
final Context context = RuntimeEnvironment.application.getApplicationContext();
webRoot = context.getFilesDir();
FileUtils.deleteDirectory(webRoot);
@@ -99,7 +105,7 @@ public class LocalHTTPDTest {
localHttpd = new LocalHTTPD(
context,
"localhost",
- 8888,
+ port,
webRoot,
false);
try {
@@ -112,7 +118,9 @@ public class LocalHTTPDTest {
});
serverStartThread.start();
// give the server some tine to start.
- Thread.sleep(100);
+ do {
+ Thread.sleep(100);
+ } while (!Utils.isServerSocketInUse(port));
}
@After
@@ -125,7 +133,7 @@ public class LocalHTTPDTest {
@Test
public void doTest404() throws Exception {
- HttpURLConnection connection = getNoKeepAliveConnection("http://localhost:8888/xxx/yyy.html");
+ HttpURLConnection connection = getNoKeepAliveConnection(baseUrl + "/xxx/yyy.html");
connection.setReadTimeout(5000);
connection.connect();
Assert.assertEquals(404, connection.getResponseCode());
@@ -134,14 +142,14 @@ public class LocalHTTPDTest {
@Test
public void doSomeBasicTest() throws Exception {
- URL url = new URL("http://localhost:8888/testdir/test.html");
+ URL url = new URL(baseUrl + "/testdir/test.html");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
assertEquals(200, connection.getResponseCode());
String string = IOUtils.toString(connection.getInputStream(), "UTF-8");
Assert.assertEquals("\n\ndummy\n\n\n\t
it works
\n\n", string);
connection.disconnect();
- url = new URL("http://localhost:8888/");
+ url = new URL(baseUrl + "/");
connection = (HttpURLConnection) url.openConnection();
assertEquals(200, connection.getResponseCode());
string = IOUtils.toString(connection.getInputStream(), "UTF-8");
@@ -149,7 +157,7 @@ public class LocalHTTPDTest {
assertTrue(string.indexOf("testdir") > 0);
connection.disconnect();
- url = new URL("http://localhost:8888/testdir");
+ url = new URL(baseUrl + "/testdir");
connection = (HttpURLConnection) url.openConnection();
assertEquals(200, connection.getResponseCode());
string = IOUtils.toString(connection.getInputStream(), "UTF-8");
@@ -158,7 +166,7 @@ public class LocalHTTPDTest {
IOUtils.copy(classLoader.getResourceAsStream("index.microg.jar"),
new FileOutputStream(new File(webRoot, "index.microg.jar")));
- url = new URL("http://localhost:8888/index.microg.jar");
+ url = new URL(baseUrl + "/index.microg.jar");
connection = (HttpURLConnection) url.openConnection();
assertEquals(200, connection.getResponseCode());
byte[] actual = IOUtils.toByteArray(connection.getInputStream());
@@ -168,7 +176,7 @@ public class LocalHTTPDTest {
IOUtils.copy(classLoader.getResourceAsStream("extendedPerms.xml"),
new FileOutputStream(new File(webRoot, "extendedPerms.xml")));
- url = new URL("http://localhost:8888/extendedPerms.xml");
+ url = new URL(baseUrl + "/extendedPerms.xml");
connection = (HttpURLConnection) url.openConnection();
assertEquals(200, connection.getResponseCode());
actual = IOUtils.toByteArray(connection.getInputStream());
@@ -183,7 +191,7 @@ public class LocalHTTPDTest {
String mimeType = "application/vnd.android.package-archive";
IOUtils.copy(classLoader.getResourceAsStream(fileName),
new FileOutputStream(new File(webRoot, fileName)));
- URL url = new URL("http://localhost:8888/" + fileName);
+ URL url = new URL(baseUrl + "/" + fileName);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
assertEquals(200, connection.getResponseCode());
@@ -197,7 +205,7 @@ public class LocalHTTPDTest {
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
new FileOutputStream(indexFile));
- URL url = new URL("http://localhost:8888/");
+ URL url = new URL(baseUrl + "/");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
String mimeType = "text/html";
@@ -211,12 +219,11 @@ public class LocalHTTPDTest {
assertEquals(200, connection.getResponseCode());
connection.disconnect();
- Thread.sleep(100000);
}
@Test
public void testPostRequest() throws IOException {
- URL url = new URL("http://localhost:8888/request-swap");
+ URL url = new URL(baseUrl + "/request-swap");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
@@ -224,7 +231,7 @@ public class LocalHTTPDTest {
OutputStream outputStream = connection.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
- writer.write("repo=http://localhost:8888");
+ writer.write("repo=" + baseUrl);
writer.flush();
writer.close();
outputStream.close();
@@ -235,14 +242,14 @@ public class LocalHTTPDTest {
@Test
public void testBadPostRequest() throws IOException {
- URL url = new URL("http://localhost:8888/request-swap");
+ URL url = new URL(baseUrl + "/request-swap");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
OutputStream outputStream = connection.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
- writer.write("repolkasdfkjhttp://localhost:8888");
+ writer.write("repolkasdfkj" + baseUrl);
writer.flush();
writer.close();
outputStream.close();
@@ -294,7 +301,7 @@ public class LocalHTTPDTest {
@Test
public void testURLContainsParentDirectory() throws IOException {
HttpURLConnection connection = null;
- URL url = new URL("http://localhost:8888/testdir/../index.html");
+ URL url = new URL(baseUrl + "/testdir/../index.html");
try {
connection = (HttpURLConnection) url.openConnection();
Assert.assertEquals("The response status should be 403(Forbidden), " + "since the server won't serve requests with '../' due to security reasons",
@@ -315,7 +322,7 @@ public class LocalHTTPDTest {
assertTrue(indexDir.mkdir());
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
new FileOutputStream(new File(indexDir, "index.html")));
- URL url = new URL("http://localhost:8888/" + dirName);
+ URL url = new URL(baseUrl + "/" + dirName);
connection = (HttpURLConnection) url.openConnection();
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory," + " the server should respond with that file",
@@ -323,7 +330,7 @@ public class LocalHTTPDTest {
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
new FileOutputStream(new File(webRoot, "index.html")));
- url = new URL("http://localhost:8888/");
+ url = new URL(baseUrl + "/");
connection = (HttpURLConnection) url.openConnection();
responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory,"
@@ -340,7 +347,7 @@ public class LocalHTTPDTest {
public void testRangeHeaderWithStartPositionOnly() throws IOException {
HttpURLConnection connection = null;
try {
- connection = getNoKeepAliveConnection("http://localhost:8888/testdir/test.html");
+ connection = getNoKeepAliveConnection(baseUrl + "/testdir/test.html");
connection.addRequestProperty("range", "bytes=10-");
connection.setReadTimeout(5000);
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
@@ -365,7 +372,7 @@ public class LocalHTTPDTest {
public void testRangeStartGreaterThanFileLength() throws IOException {
HttpURLConnection connection = null;
try {
- URL url = new URL("http://localhost:8888/testdir/test.html");
+ URL url = new URL(baseUrl + "/testdir/test.html");
connection = (HttpURLConnection) url.openConnection();
connection.addRequestProperty("range", "bytes=1000-");
connection.connect();
@@ -384,7 +391,7 @@ public class LocalHTTPDTest {
public void testRangeHeaderWithStartAndEndPosition() throws IOException {
HttpURLConnection connection = null;
try {
- URL url = new URL("http://localhost:8888/testdir/test.html");
+ URL url = new URL(baseUrl + "/testdir/test.html");
connection = (HttpURLConnection) url.openConnection();
connection.addRequestProperty("range", "bytes=10-40");
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
@@ -412,7 +419,7 @@ public class LocalHTTPDTest {
while (status == -1) {
System.out.println("testIfNoneMatchHeader connect attempt");
try {
- connection = getNoKeepAliveConnection("http://localhost:8888/testdir/test.html");
+ connection = getNoKeepAliveConnection(baseUrl + "/testdir/test.html");
connection.setRequestProperty("if-none-match", "*");
connection.connect();
status = connection.getResponseCode();
@@ -430,7 +437,7 @@ public class LocalHTTPDTest {
public void testRangeHeaderAndIfNoneMatchHeader() throws IOException {
HttpURLConnection connection = null;
try {
- URL url = new URL("http://localhost:8888/testdir/test.html");
+ URL url = new URL(baseUrl + "/testdir/test.html");
connection = (HttpURLConnection) url.openConnection();
connection.addRequestProperty("range", "bytes=10-20");
connection.addRequestProperty("if-none-match", "*");
diff --git a/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java
new file mode 100644
index 000000000..1436c5040
--- /dev/null
+++ b/app/src/testFull/java/org/fdroid/fdroid/updater/SwapRepoTest.java
@@ -0,0 +1,187 @@
+package org.fdroid.fdroid.updater;
+
+import android.content.ContentResolver;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import org.apache.commons.net.util.SubnetUtils;
+import org.fdroid.fdroid.FDroidApp;
+import org.fdroid.fdroid.Hasher;
+import org.fdroid.fdroid.IndexUpdater;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.TestUtils;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.DBHelper;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.data.Schema;
+import org.fdroid.fdroid.data.ShadowApp;
+import org.fdroid.fdroid.data.TempAppProvider;
+import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
+import org.fdroid.fdroid.localrepo.LocalRepoManager;
+import org.fdroid.fdroid.localrepo.LocalRepoService;
+import org.fdroid.fdroid.net.LocalHTTPD;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+/**
+ * This test almost works, it needs to have the {@link android.content.ContentProvider}
+ * and {@link ContentResolver} stuff worked out. It currently fails as
+ * {@code updater.update()}.
+ */
+@Ignore
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowApp.class)
+public class SwapRepoTest {
+
+ private LocalHTTPD localHttpd;
+
+ protected ShadowContentResolver shadowContentResolver;
+ protected ContentResolver contentResolver;
+ protected ContextWrapper context;
+
+ @Before
+ public void setUp() {
+ ShadowLog.stream = System.out;
+
+ contentResolver = RuntimeEnvironment.application.getContentResolver();
+ shadowContentResolver = Shadows.shadowOf(contentResolver);
+ context = new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) {
+ @Override
+ public ContentResolver getContentResolver() {
+ return contentResolver;
+ }
+ };
+
+ TestUtils.registerContentProvider(ApkProvider.getAuthority(), ApkProvider.class);
+ TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class);
+ TestUtils.registerContentProvider(RepoProvider.getAuthority(), RepoProvider.class);
+ TestUtils.registerContentProvider(TempAppProvider.getAuthority(), TempAppProvider.class);
+
+ Preferences.setupForTests(context);
+ }
+
+ @After
+ public final void tearDownBase() {
+ DBHelper.clearDbHelperSingleton();
+ }
+
+ /**
+ * @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run()
+ */
+ @Test
+ public void testSwap()
+ throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
+
+ PackageManager packageManager = context.getPackageManager();
+ ShadowPackageManager shadowPackageManager = shadowOf(packageManager);
+ ApplicationInfo appInfo = new ApplicationInfo();
+ appInfo.flags = 0;
+ appInfo.packageName = context.getPackageName();
+ appInfo.minSdkVersion = 10;
+ appInfo.targetSdkVersion = 23;
+ appInfo.sourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
+ appInfo.publicSourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
+ System.out.println("appInfo.sourceDir " + appInfo.sourceDir);
+ appInfo.name = "F-Droid";
+
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = appInfo.packageName;
+ packageInfo.applicationInfo = appInfo;
+ packageInfo.versionCode = 1002001;
+ packageInfo.versionName = "1.2-fake";
+ shadowPackageManager.addPackage(packageInfo);
+
+ try {
+ FDroidApp.initWifiSettings();
+ FDroidApp.ipAddressString = "127.0.0.1";
+ FDroidApp.subnetInfo = new SubnetUtils("127.0.0.0/8").getInfo();
+ FDroidApp.repo.name = "test";
+ FDroidApp.repo.address = "http://" + FDroidApp.ipAddressString + ":" + FDroidApp.port + "/fdroid/repo";
+
+ LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
+ File indexJarFile = LocalRepoManager.get(context).getIndexJar();
+ System.out.println("indexJarFile:" + indexJarFile);
+ assertTrue(indexJarFile.isFile());
+
+ localHttpd = new LocalHTTPD(
+ context,
+ FDroidApp.ipAddressString,
+ FDroidApp.port,
+ LocalRepoManager.get(context).getWebRoot(),
+ false);
+ localHttpd.start();
+ Thread.sleep(100); // give the server some tine to start.
+ assertTrue(localHttpd.isAlive());
+
+ LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
+ Certificate localCert = localRepoKeyStore.getCertificate();
+ String signingCert = Hasher.hex(localCert);
+ assertFalse(TextUtils.isEmpty(signingCert));
+ assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
+
+ Repo repo = MultiIndexUpdaterTest.createRepo(FDroidApp.repo.name, FDroidApp.repo.address,
+ context, signingCert);
+ IndexUpdater updater = new IndexUpdater(context, repo);
+ updater.update();
+ assertTrue(updater.hasChanged());
+ updater.processDownloadedFile(indexJarFile);
+
+ boolean foundRepo = false;
+ for (Repo repoFromDb : RepoProvider.Helper.all(context)) {
+ if (TextUtils.equals(repo.address, repoFromDb.address)) {
+ foundRepo = true;
+ repo = repoFromDb;
+ }
+ }
+ assertTrue(foundRepo);
+
+ assertNotEquals(-1, repo.getId());
+ List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
+ assertEquals(1, apks.size());
+ for (Apk apk : apks) {
+ System.out.println(apk);
+ }
+ //MultiIndexUpdaterTest.assertApksExist(apks, context.getPackageName(), new int[]{BuildConfig.VERSION_CODE});
+ Thread.sleep(10000);
+ } finally {
+ if (localHttpd != null) {
+ localHttpd.stop();
+ }
+ }
+ }
+
+ class TestLocalRepoService extends LocalRepoService {
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ super.onHandleIntent(intent);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/check-string-maxlength.py b/tools/check-string-maxlength.py
index 8be69cc0a..7f3b92e8f 100755
--- a/tools/check-string-maxlength.py
+++ b/tools/check-string-maxlength.py
@@ -13,6 +13,15 @@ maxlengths = {
"menu_uninstall": 20,
"nearby_splash__find_people_button": 30,
"nearby_splash__request_permission": 30,
+ "swap": 25,
+ "swap_nfc_title": 25,
+ "swap_choose_apps": 25,
+ "swap_confirm": 25,
+ "swap_connecting": 25,
+ "swap_nearby": 25,
+ "swap_scan_qr": 18,
+ "swap_send_fdroid": 18,
+ "swap_success": 25,
"update_all": 20,
"updates__hide_updateable_apps": 35,
"updates__show_updateable_apps": 35,