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 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 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 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 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 subscriber; - - protected PeerFinder(Context context, Subscriber 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 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 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. + *

    + *
  1. If we directly selected a peer to swap with initially, we will skip straight to getting + * the list of apps from that device.
  2. + *
  3. 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.
  4. + *
*/ 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 @@ - -