Merge branch 'major-swap-overhaul' into 'master'
major swap overhaul See merge request fdroid/fdroidclient!825
This commit is contained in:
commit
f69b38aad5
@ -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'
|
||||
|
||||
|
@ -40,6 +40,8 @@
|
||||
|
||||
<issue id="PluralsCandidate" severity="error"/>
|
||||
<issue id="HardcodedText" severity="error"/>
|
||||
<issue id="RtlCompat" severity="error"/>
|
||||
<issue id="RtlEnabled" severity="error"/>
|
||||
|
||||
<!-- both the correct and deprecated locales need to be present for
|
||||
them to be recognized on all devices -->
|
||||
|
372
app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
Normal file
372
app/src/androidTest/java/org/fdroid/fdroid/Netstat.java
Normal file
@ -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):
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* 00000150:00000000 01:00000019 00000000
|
||||
* | | | | |--> number of unrecovered RTO timeouts
|
||||
* | | | |----------> number of jiffies until timer expires
|
||||
* | | |----------------> timer_active (see below)
|
||||
* | |----------------------> receive-queue
|
||||
* |-------------------------------> transmit-queue
|
||||
* <p>
|
||||
* 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<Connection> getConnections() {
|
||||
|
||||
final ArrayList<Connection> 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;
|
||||
/**
|
||||
* <code>serialVersionUID</code>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
@ -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<Apk> 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<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(mainIntent, 0);
|
||||
HashSet<String> 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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
};
|
||||
}
|
||||
|
@ -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"/>
|
||||
<service android:name=".localrepo.SwapService"/>
|
||||
|
||||
<service android:name=".localrepo.LocalHTTPDManager"/>
|
||||
<service
|
||||
android:name=".localrepo.CacheSwapAppsService"
|
||||
android:name=".localrepo.LocalRepoService"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".localrepo.TreeUriScannerIntentService"
|
||||
|
@ -0,0 +1,178 @@
|
||||
package org.fdroid.fdroid.localrepo;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
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.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.localrepo.peers.BluetoothPeer;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Manage the {@link android.bluetooth.BluetoothAdapter}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.
|
||||
*
|
||||
* @see BonjourManager
|
||||
* @see LocalRepoManager
|
||||
*/
|
||||
public class BluetoothManager {
|
||||
private static final String TAG = "BluetoothManager";
|
||||
|
||||
public static final String ACTION_FOUND = "BluetoothNewPeer";
|
||||
public static final String EXTRA_PEER = "extraBluetoothPeer";
|
||||
|
||||
public static final String ACTION_STATUS = "BluetoothStatus";
|
||||
public static final String EXTRA_STATUS = "BluetoothStatusExtra";
|
||||
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_ERROR = 0xffff;
|
||||
|
||||
private static final int STOP = 5709;
|
||||
|
||||
private static WeakReference<Context> 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);
|
||||
}
|
||||
}
|
@ -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> 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<String, String> 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<String, App> apps = new HashMap<>();
|
||||
private final Map<String, App> 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;
|
||||
}
|
||||
|
@ -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.
|
||||
* <p/>
|
||||
* 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<String> 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));
|
||||
}
|
||||
}
|
@ -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<String> appsToSwap = new HashSet<>();
|
||||
private final Set<Peer> activePeers = new HashSet<>();
|
||||
|
||||
/**
|
||||
* A cache of parsed APKs from the file system.
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, App> 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<Peer> 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.
|
||||
* <p>
|
||||
* The returned object will scan for peers on a background thread, and emit
|
||||
* found peers on the mian thread.
|
||||
* <p>
|
||||
* 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<Peer> 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<String> getAppsToSwap() {
|
||||
return appsToSwap;
|
||||
}
|
||||
|
||||
public void refreshSwap() {
|
||||
if (peer != null) {
|
||||
connectTo(peer, false);
|
||||
}
|
||||
@NonNull
|
||||
public Set<Peer> 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<Void, Void, Void>() {
|
||||
@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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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<Peer> createBluetoothObservable(final Context context) {
|
||||
return Observable.create(new Observable.OnSubscribe<Peer>() {
|
||||
@Override
|
||||
public void call(Subscriber<? super Peer> subscriber) {
|
||||
final BluetoothFinder finder = new BluetoothFinder(context, subscriber);
|
||||
|
||||
subscriber.add(Subscriptions.create(new Action0() {
|
||||
@Override
|
||||
public void call() {
|
||||
finder.cancel();
|
||||
}
|
||||
}));
|
||||
|
||||
finder.scan();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final String TAG = "BluetoothFinder";
|
||||
|
||||
private final BluetoothAdapter adapter;
|
||||
|
||||
private BluetoothFinder(Context context, Subscriber<? super Peer> subscriber) {
|
||||
super(context, subscriber);
|
||||
adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
|
||||
private BroadcastReceiver deviceFoundReceiver;
|
||||
private BroadcastReceiver scanCompleteReceiver;
|
||||
|
||||
private void scan() {
|
||||
|
||||
if (adapter == null) {
|
||||
Log.i(TAG, "Not scanning for bluetooth peers to swap with, couldn't find a bluetooth adapter on this device.");
|
||||
return;
|
||||
}
|
||||
|
||||
isScanning = true;
|
||||
|
||||
if (deviceFoundReceiver == null) {
|
||||
deviceFoundReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
onDeviceFound(device);
|
||||
}
|
||||
}
|
||||
};
|
||||
context.registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
|
||||
}
|
||||
|
||||
if (scanCompleteReceiver == null) {
|
||||
scanCompleteReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (isScanning) {
|
||||
Utils.debugLog(TAG, "Scan complete, but we haven't been asked to stop scanning yet, so will restart scan.");
|
||||
startDiscovery();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Unregister this receiver at the appropriate time.
|
||||
context.registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
|
||||
}
|
||||
|
||||
startDiscovery();
|
||||
}
|
||||
|
||||
private void startDiscovery() {
|
||||
|
||||
if (adapter.isDiscovering()) {
|
||||
// TODO: Can we reset the discovering timeout, so that it doesn't, e.g. time out in 3
|
||||
// seconds because we had already almost completed the previous scan? We could
|
||||
// cancelDiscovery(), but then it will probably prompt the user again.
|
||||
Utils.debugLog(TAG, "Requested bluetooth scan when already scanning, so will ignore request.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!adapter.startDiscovery()) {
|
||||
Log.e(TAG, "Couldn't start bluetooth scanning.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
if (adapter != null) {
|
||||
Utils.debugLog(TAG, "Stopping bluetooth discovery.");
|
||||
adapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
isScanning = false;
|
||||
}
|
||||
|
||||
private void onDeviceFound(BluetoothDevice device) {
|
||||
if (device != null && device.getName() != null &&
|
||||
(device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA ||
|
||||
device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA ||
|
||||
device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.PHONE_SMART)) {
|
||||
subscriber.onNext(new BluetoothPeer(device));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
|
@ -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<Peer> createBonjourObservable(final Context context) {
|
||||
return Observable.create(new Observable.OnSubscribe<Peer>() {
|
||||
@Override
|
||||
public void call(Subscriber<? super Peer> subscriber) {
|
||||
final BonjourFinder finder = new BonjourFinder(context, subscriber);
|
||||
|
||||
subscriber.add(Subscriptions.create(new Action0() {
|
||||
@Override
|
||||
public void call() {
|
||||
finder.cancel();
|
||||
}
|
||||
}));
|
||||
|
||||
finder.scan();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final String TAG = "BonjourFinder";
|
||||
|
||||
private static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
|
||||
private static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
|
||||
|
||||
private JmDNS jmdns;
|
||||
private WifiManager wifiManager;
|
||||
private WifiManager.MulticastLock multicastLock;
|
||||
|
||||
private BonjourFinder(Context context, Subscriber<? super Peer> subscriber) {
|
||||
super(context, subscriber);
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
|
||||
Utils.debugLog(TAG, "Requested Bonjour (mDNS) scan for peers.");
|
||||
|
||||
if (wifiManager == null) {
|
||||
wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifiManager.createMulticastLock(context.getPackageName());
|
||||
multicastLock.setReferenceCounted(false);
|
||||
}
|
||||
|
||||
if (isScanning) {
|
||||
Utils.debugLog(TAG, "Requested Bonjour scan, but already scanning. But we will still try to explicitly scan for services.");
|
||||
return;
|
||||
}
|
||||
|
||||
isScanning = true;
|
||||
multicastLock.acquire();
|
||||
|
||||
try {
|
||||
Utils.debugLog(TAG, "Searching for Bonjour (mDNS) clients...");
|
||||
jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
|
||||
} catch (IOException e) {
|
||||
subscriber.onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE);
|
||||
jmdns.addServiceListener(HTTP_SERVICE_TYPE, this);
|
||||
jmdns.addServiceListener(HTTPS_SERVICE_TYPE, this);
|
||||
listServices();
|
||||
}
|
||||
|
||||
private void listServices() {
|
||||
Utils.debugLog(TAG, "Explicitly querying for services, in addition to waiting for notifications.");
|
||||
addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE));
|
||||
addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceAdded(final ServiceEvent event) {
|
||||
// TODO: Get clarification, but it looks like this is:
|
||||
// 1) Identifying that there is _a_ bonjour service available
|
||||
// 2) Adding it to the list to give some sort of feedback to the user
|
||||
// 3) Requesting more detailed info in an async manner
|
||||
// 4) If that is in fact an fdroid repo (after requesting info), then add it again
|
||||
// so that more detailed info can be shown to the user.
|
||||
//
|
||||
// If so, when is the old one removed?
|
||||
addFDroidService(event.getInfo());
|
||||
|
||||
Utils.debugLog(TAG, "Found JmDNS service, now requesting further details of service");
|
||||
jmdns.requestServiceInfo(event.getType(), event.getName(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
addFDroidService(event.getInfo());
|
||||
}
|
||||
|
||||
private void addFDroidServices(ServiceInfo[] services) {
|
||||
for (ServiceInfo info : services) {
|
||||
addFDroidService(info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the fact that a Bonjour peer was found to swap with.
|
||||
* Checks that the service is an F-Droid service, and also that it is not the F-Droid service
|
||||
* for this device (by comparing its signing fingerprint to our signing fingerprint).
|
||||
*/
|
||||
private void addFDroidService(ServiceInfo serviceInfo) {
|
||||
final String type = serviceInfo.getPropertyString("type");
|
||||
final String fingerprint = serviceInfo.getPropertyString("fingerprint");
|
||||
final boolean isFDroid = type != null && type.startsWith("fdroidrepo");
|
||||
final boolean isSelf = FDroidApp.repo != null && fingerprint != null && fingerprint.equalsIgnoreCase(FDroidApp.repo.fingerprint);
|
||||
if (isFDroid && !isSelf) {
|
||||
Utils.debugLog(TAG, "Found F-Droid swap Bonjour service:\n" + serviceInfo);
|
||||
subscriber.onNext(new BonjourPeer(serviceInfo));
|
||||
} else {
|
||||
if (isSelf) {
|
||||
Utils.debugLog(TAG, "Ignoring Bonjour service because it belongs to this device:\n" + serviceInfo);
|
||||
} else {
|
||||
Utils.debugLog(TAG, "Ignoring Bonjour service because it doesn't look like an F-Droid swap repo:\n" + serviceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
Utils.debugLog(TAG, "Cancelling BonjourFinder, releasing multicast lock, removing jmdns service listeners");
|
||||
|
||||
if (multicastLock != null) {
|
||||
multicastLock.release();
|
||||
}
|
||||
|
||||
isScanning = false;
|
||||
|
||||
if (jmdns == null) {
|
||||
return;
|
||||
}
|
||||
jmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
|
||||
jmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
|
||||
jmdns = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
package org.fdroid.fdroid.localrepo.peers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.Subscriber;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Searches for other devices in the vicinity, using specific technologies.
|
||||
* Once found, emits a {@link Peer} to interested {@link Subscriber}s.
|
||||
*/
|
||||
public abstract class PeerFinder {
|
||||
|
||||
protected boolean isScanning;
|
||||
protected final Context context;
|
||||
protected final Subscriber<? super Peer> subscriber;
|
||||
|
||||
protected PeerFinder(Context context, Subscriber<? super Peer> subscriber) {
|
||||
this.context = context;
|
||||
this.subscriber = subscriber;
|
||||
}
|
||||
|
||||
public static Observable<Peer> createObservable(final Context context) {
|
||||
return Observable.merge(
|
||||
BluetoothFinder.createBluetoothObservable(context).subscribeOn(Schedulers.newThread()),
|
||||
BonjourFinder.createBonjourObservable(context).subscribeOn(Schedulers.newThread())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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<String, String> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
start();
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void ensureRunning() {
|
||||
if (!isConnected()) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureRunningInBackground() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
ensureRunning();
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
public void stopInBackground() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
stop();
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
}
|
@ -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<Boolean, Boolean, Boolean>() {
|
||||
@Override
|
||||
public Boolean call(Boolean webServerTask, Boolean bonjourServiceTask) {
|
||||
return bonjourServiceTask && webServerTask;
|
||||
}
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeOn(Schedulers.newThread())
|
||||
.subscribe(new Action1<Boolean>() {
|
||||
@Override
|
||||
public void call(Boolean success) {
|
||||
setConnected(success);
|
||||
}
|
||||
},
|
||||
new Action1<Throwable>() {
|
||||
@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<Boolean> getBonjourTask() {
|
||||
return new Single.OnSubscribe<Boolean>() {
|
||||
@Override
|
||||
public void call(SingleSubscriber<? super Boolean> singleSubscriber) {
|
||||
bonjourBroadcast.start();
|
||||
|
||||
// TODO: Be more intelligent about failures here so that we can invoke
|
||||
// singleSubscriber.onError() in the appropriate circumstances.
|
||||
singleSubscriber.onSuccess(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Thread} for the webserver to run on. If successful, it will also
|
||||
* populate the webServerThreadHandler property and bind it to that particular thread. This
|
||||
* allows messages to be sent to the webserver thread by posting messages to that handler.
|
||||
*/
|
||||
private Single.OnSubscribe<Boolean> getWebServerTask() {
|
||||
return new Single.OnSubscribe<Boolean>() {
|
||||
@Override
|
||||
public void call(final SingleSubscriber<? super Boolean> singleSubscriber) {
|
||||
new Thread(new Runnable() {
|
||||
// Tell Eclipse this is not a leak because of Looper use.
|
||||
@SuppressLint("HandlerLeak")
|
||||
@Override
|
||||
public void run() {
|
||||
localHttpd = new LocalHTTPD(
|
||||
context,
|
||||
FDroidApp.ipAddressString,
|
||||
FDroidApp.port,
|
||||
context.getFilesDir(),
|
||||
Preferences.get().isLocalRepoHttpsEnabled());
|
||||
|
||||
Looper.prepare(); // must be run before creating a Handler
|
||||
webServerThreadHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
|
||||
localHttpd.stop();
|
||||
Looper looper = Looper.myLooper();
|
||||
if (looper == null) {
|
||||
Log.e(TAG, "Looper.myLooper() was null for sum reason while shutting down the swap webserver.");
|
||||
} else {
|
||||
looper.quit();
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
Utils.debugLog(TAG, "Starting swap webserver...");
|
||||
localHttpd.start();
|
||||
Utils.debugLog(TAG, "Swap webserver started.");
|
||||
singleSubscriber.onSuccess(true);
|
||||
} catch (BindException e) {
|
||||
int prev = FDroidApp.port;
|
||||
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
|
||||
WifiStateChangeService.start(context, null);
|
||||
singleSubscriber.onError(new Exception("port " + prev + " occupied, trying on " + FDroidApp.port + "!"));
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Could not start local repo HTTP server", e);
|
||||
singleSubscriber.onError(e);
|
||||
}
|
||||
Looper.loop(); // start the message receiving loop
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
sendBroadcast(SwapService.EXTRA_STOPPING);
|
||||
if (webServerThreadHandler == null) {
|
||||
Log.i(TAG, "null handler in stopWebServer");
|
||||
} else {
|
||||
Utils.debugLog(TAG, "Sending message to swap webserver to stop it.");
|
||||
Message msg = webServerThreadHandler.obtainMessage();
|
||||
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
|
||||
webServerThreadHandler.sendMessage(msg);
|
||||
}
|
||||
|
||||
// Stop the Bonjour stuff after asking the webserver to stop. This is not required in this
|
||||
// order, but it helps. In practice, the Bonjour stuff takes a second or two to stop. This
|
||||
// should give enough time for the message we posted above to reach the web server thread
|
||||
// and for the webserver to thus be stopped.
|
||||
bonjourBroadcast.stop();
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
}
|
@ -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> context;
|
||||
|
||||
protected List<File> 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());
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<ClientConnection> 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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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<Peer> {
|
||||
class PeopleNearbyAdapter extends ArrayAdapter<Peer> {
|
||||
|
||||
PeopleNearbyAdapter(Context context) {
|
||||
super(context, 0, new ArrayList<Peer>());
|
||||
@ -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<Peer> onPeerFound = new Subscriber<Peer>() {
|
||||
|
||||
@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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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).
|
||||
* <p>
|
||||
* 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:
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
|
@ -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<Cursor> {
|
||||
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<Cursor> loader, Cursor cursor) {
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
adapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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<String> 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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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"
|
||||
/>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.ConfirmReceiveView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
@ -54,9 +54,8 @@
|
||||
android:layout_marginTop="45dp">
|
||||
<!-- 80px * 0.56 = 45dp -->
|
||||
|
||||
<!-- TODO: Remove associated style files style="@style/SwapTheme.Wizard.ReceiveSwap.Deny"-->
|
||||
<Button
|
||||
android:id="@+id/no_button"
|
||||
android:id="@+id/confirm_receive_yes"
|
||||
android:text="@string/no"
|
||||
android:backgroundTint="@color/swap_deny"
|
||||
android:layout_width="wrap_content"
|
||||
@ -65,9 +64,8 @@
|
||||
android:layout_marginRight="25dp"
|
||||
tools:ignore="UnusedAttribute"/>
|
||||
|
||||
<!-- TODO: Remove associated style files style="@style/SwapTheme.Wizard.ReceiveSwap.Confirm" -->
|
||||
<Button
|
||||
android:id="@+id/yes_button"
|
||||
android:id="@+id/confirm_receive_no"
|
||||
android:text="@string/yes"
|
||||
android:backgroundTint="@color/swap_light_blue"
|
||||
android:layout_width="wrap_content"
|
||||
@ -76,4 +74,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.fdroid.fdroid.views.swap.ConfirmReceiveView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.ConnectingView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
swap:toolbarColor="@color/swap_bright_blue"
|
||||
@ -10,7 +10,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heading"
|
||||
android:id="@+id/progress_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
@ -25,28 +25,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_below="@+id/heading"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:id="@+id/error"
|
||||
android:textSize="20sp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/swap_connection_misc_error"
|
||||
android:visibility="gone"
|
||||
android:padding="30dp"/>
|
||||
android:layout_below="@+id/progress_text"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_below="@+id/error"
|
||||
android:id="@+id/back"
|
||||
android:layout_below="@+id/progress_text"
|
||||
android:id="@+id/try_again"
|
||||
android:backgroundTint="@color/swap_light_blue"
|
||||
android:textColor="@android:color/white"
|
||||
android:visibility="gone"
|
||||
android:text="@string/back"/>
|
||||
android:text="@string/try_again"/>
|
||||
|
||||
</org.fdroid.fdroid.views.swap.ConnectingView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
||||
|
@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
swap:toolbarTitle="@string/swap"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/swap_blue"
|
||||
android:paddingTop="38.8dp"> <!-- 69px * 96dpi / 160dpi -->
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/loading"
|
||||
android:textSize="18sp"
|
||||
android:layout_below="@+id/progress"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.JoinWifiView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
@ -57,4 +57,4 @@
|
||||
android:paddingBottom="20dp"/>
|
||||
<!-- android:layout_above="@id/btn_learn_more_about_wifi" -->
|
||||
|
||||
</org.fdroid.fdroid.views.swap.JoinWifiView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.NfcView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
swap:toolbarTitle="@string/swap_nfc_title"
|
||||
@ -35,4 +35,4 @@
|
||||
android:layout_below="@+id/text_description"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
</org.fdroid.fdroid.views.swap.NfcView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.SendFDroidView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
@ -50,4 +50,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</org.fdroid.fdroid.views.swap.SendFDroidView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
@ -1,9 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- TODO: Add paddingStart in places where there is only paddingLeft. However Android Studio lint
|
||||
gives an error, which is discussed here:
|
||||
http://stackoverflow.com/questions/27449776/conflicting-lint-messages-regarding-paddingstart-usage?lq=1
|
||||
-->
|
||||
<org.fdroid.fdroid.views.swap.StartSwapView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
@ -31,6 +26,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:tint="@color/swap_grey_icon"
|
||||
android:contentDescription="@string/use_bluetooth"
|
||||
android:src="@drawable/ic_bluetooth_white"/>
|
||||
|
||||
<LinearLayout
|
||||
@ -46,7 +42,7 @@
|
||||
android:id="@+id/bluetooth_visible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/swap_visible_bluetooth"
|
||||
tools:text="@string/swap_not_visible_bluetooth"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
@ -79,6 +75,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:tint="@color/swap_grey_icon"
|
||||
android:contentDescription="@string/wifi"
|
||||
android:src="@drawable/ic_network_wifi_white"/>
|
||||
|
||||
<LinearLayout
|
||||
@ -94,7 +91,7 @@
|
||||
android:id="@+id/wifi_visible"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/swap_not_visible_wifi"
|
||||
tools:text="@string/swap_starting"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<TextView
|
||||
@ -117,7 +114,6 @@
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="false"
|
||||
android:id="@+id/switch_wifi"/>
|
||||
|
||||
</LinearLayout>
|
||||
@ -168,11 +164,12 @@
|
||||
android:drawablePadding="10dp"
|
||||
android:paddingLeft="25dp"
|
||||
android:paddingRight="25dp"
|
||||
android:paddingStart="25dp"
|
||||
android:paddingEnd="25dp"
|
||||
android:background="@android:color/transparent"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_qr_scanner"
|
||||
android:id="@+id/btn_scan_qr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableLeft="@drawable/ic_qr_grey"
|
||||
@ -181,6 +178,7 @@
|
||||
android:drawablePadding="10dp"
|
||||
android:paddingLeft="25dp"
|
||||
android:paddingRight="25dp"
|
||||
android:paddingStart="25dp"
|
||||
android:paddingEnd="25dp"
|
||||
android:background="@android:color/transparent"/>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.WifiQrView
|
||||
<org.fdroid.fdroid.localrepo.SwapView
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:swap="http://schemas.android.com/apk/res-auto"
|
||||
@ -57,4 +57,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</org.fdroid.fdroid.views.swap.WifiQrView>
|
||||
</org.fdroid.fdroid.localrepo.SwapView>
|
@ -487,7 +487,12 @@ public class UpdateService extends JobIntentService {
|
||||
}
|
||||
} catch (IndexUpdater.UpdateException e) {
|
||||
errorRepos++;
|
||||
repoErrors.add(e.getLocalizedMessage());
|
||||
Throwable cause = e.getCause();
|
||||
if (cause == null) {
|
||||
repoErrors.add(e.getLocalizedMessage());
|
||||
} else {
|
||||
repoErrors.add(e.getLocalizedMessage() + " ⇨ " + cause.getLocalizedMessage());
|
||||
}
|
||||
Log.e(TAG, "Error updating repository " + repo.address);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@ -63,6 +63,9 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@ -887,4 +890,26 @@ public final class Utils {
|
||||
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
|
||||
swipeLayout.setColorSchemeColors(typedValue.data);
|
||||
}
|
||||
|
||||
public static boolean canConnectToSocket(String host, int port) {
|
||||
try {
|
||||
Socket socket = new Socket();
|
||||
socket.connect(new InetSocketAddress(host, port), 5);
|
||||
socket.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// Could not connect.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isServerSocketInUse(int port) {
|
||||
try {
|
||||
(new ServerSocket(port)).close();
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
// Could not connect.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -385,13 +385,16 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
* exists.
|
||||
*/
|
||||
@Nullable
|
||||
public static App getInstance(Context context, PackageManager pm, String packageName)
|
||||
public static App getInstance(Context context, PackageManager pm, InstalledApp installedApp, String packageName)
|
||||
throws CertificateEncodingException, IOException, PackageManager.NameNotFoundException {
|
||||
App app = new App();
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
|
||||
SanitizedFile apkFile = SanitizedFile.knownSanitized(packageInfo.applicationInfo.publicSourceDir);
|
||||
app.installedApk = new Apk();
|
||||
if (apkFile.canRead()) {
|
||||
if (installedApp != null) {
|
||||
app.installedApk.hashType = installedApp.getHashType();
|
||||
app.installedApk.hash = installedApp.getHash();
|
||||
} else if (apkFile.canRead()) {
|
||||
String hashType = "sha256";
|
||||
String hash = Utils.getBinaryHash(apkFile, hashType);
|
||||
if (TextUtils.isEmpty(hash)) {
|
||||
|
@ -47,7 +47,7 @@ public class BluetoothConnection {
|
||||
Utils.closeQuietly(socket);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
public void close() {
|
||||
closeQuietly();
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@ -702,7 +701,7 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
if (Build.VERSION.SDK_INT < 18) {
|
||||
return BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
return ((BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter();
|
||||
return ((android.bluetooth.BluetoothManager) getSystemService(BLUETOOTH_SERVICE)).getAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,11 +92,10 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
||||
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
|
||||
|
||||
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
|
||||
public static final String ACTION_REQUEST_SWAP = "requestSwap";
|
||||
|
||||
private static final String STATE_SELECTED_MENU_ID = "selectedMenuId";
|
||||
|
||||
private static final int REQUEST_SWAP = 3;
|
||||
|
||||
private RecyclerView pager;
|
||||
private MainViewAdapter adapter;
|
||||
private BottomNavigationBar bottomNavigation;
|
||||
@ -390,10 +389,7 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
|
||||
NewRepoConfig parser = new NewRepoConfig(this, intent);
|
||||
if (parser.isValidRepo()) {
|
||||
if (parser.isFromSwap()) {
|
||||
Intent confirmIntent = new Intent(this, SwapWorkflowActivity.class);
|
||||
confirmIntent.putExtra(SwapWorkflowActivity.EXTRA_CONFIRM, true);
|
||||
confirmIntent.setData(intent.getData());
|
||||
startActivityForResult(confirmIntent, REQUEST_SWAP);
|
||||
SwapWorkflowActivity.requestSwap(this, intent.getData());
|
||||
} else {
|
||||
Intent clean = new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class);
|
||||
if (intent.hasExtra(ManageReposActivity.EXTRA_FINISH_AFTER_ADDING_REPO)) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
@ -11,6 +10,8 @@
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="4dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<LinearLayout android:id="@+id/basic_layout"
|
||||
|
@ -76,7 +76,9 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:src="@drawable/ic_cancel"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:background="@android:color/transparent"
|
||||
|
@ -296,6 +296,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="icon">Icon</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="try_again">Try again</string>
|
||||
|
||||
<string name="useTor">Use Tor</string>
|
||||
<string name="useTorSummary">Force download traffic through Tor for increased privacy. Requires Orbot</string>
|
||||
@ -455,6 +456,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<!-- This is a button label for a small button, the text needs to be under 10 characters, the shorter the better. Also, a literal translation probably will miss the point. "Try it" is more a saying than two words. Google has been pushing these short buttons, like "Got it" and "Try now" so this button is trying to match those buttons in Android. -->
|
||||
<string name="nearby_splash__request_permission">Try it</string>
|
||||
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_nfc_title">Touch to swap</string>
|
||||
<string name="swap_nfc_description">If your friend has F-Droid and NFC turned on touch your devices together.
|
||||
</string>
|
||||
@ -463,7 +465,9 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
access to the same network, one of you can create a Wi-Fi Hotspot.
|
||||
</string>
|
||||
<string name="swap_join_this_hotspot">Help your friend join your hotspot</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap">Swap apps</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_success">Swap success!</string>
|
||||
<string name="swap_no_wifi_network">No network yet</string>
|
||||
<string name="swap_active_hotspot">%1$s (your hotspot)</string>
|
||||
@ -475,14 +479,21 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="swap_dont_show_again">Don\'t show this again</string>
|
||||
<string name="swap_scan_or_type_url">One person needs to scan the code, or type the URL of the other in a browser.
|
||||
</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_choose_apps">Choose Apps</string>
|
||||
<!-- This is a button label, it must be the right size, or the layout gets messed up. It should be less than 15 characters. -->
|
||||
<string name="swap_scan_qr">Scan QR Code</string>
|
||||
<string name="swap_people_nearby">People Nearby</string>
|
||||
<string name="swap_scanning_for_peers">Searching for nearby people…</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_nearby">Nearby Swap</string>
|
||||
<string name="swap_intro">Connect and trade apps with people near you.</string>
|
||||
<string name="swap_starting">Starting…</string>
|
||||
<string name="swap_stopping">Stopping…</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
<string name="swap_visible_bluetooth">Visible via Bluetooth</string>
|
||||
<string name="swap_setting_up_bluetooth">Setting up Bluetooth…</string>
|
||||
<string name="swap_error_cannot_start_bluetooth">Cannot start Bluetooth!</string>
|
||||
<string name="swap_not_visible_bluetooth">Not visible via Bluetooth</string>
|
||||
<string name="swap_visible_wifi">Visible via Wi-Fi</string>
|
||||
<string name="swap_setting_up_wifi">Setting up Wi-Fi…</string>
|
||||
@ -490,9 +501,12 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="swap_not_visible_wifi">Not visible via Wi-Fi</string>
|
||||
<string name="swap_wifi_device_name">Device Name</string>
|
||||
<string name="swap_cant_find_peers">Can\'t find who you\'re looking for?</string>
|
||||
<!-- This is a button label, it must be the right size, or the layout gets messed up. It should be less than 15 characters. -->
|
||||
<string name="swap_send_fdroid">Send F-Droid</string>
|
||||
<string name="swap_no_peers_nearby">Could not find people nearby to swap with.</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_connecting">Connecting</string>
|
||||
<!-- This is a screen title, it should be maximum 25 characters -->
|
||||
<string name="swap_confirm">Confirm swap</string>
|
||||
<string name="swap_qr_isnt_for_swap">The QR code you scanned doesn\'t look like a swap code.</string>
|
||||
<string name="use_bluetooth">Use Bluetooth</string>
|
||||
@ -507,6 +521,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="swap_toast_invalid_url">Invalid URL for swapping: %1$s</string>
|
||||
<string name="swap_toast_hotspot_enabled">Wi-Fi Hotspot enabled</string>
|
||||
<string name="swap_toast_could_not_enable_hotspot">Could not enable Wi-Fi Hotspot!</string>
|
||||
<string name="swap_toast_closing_nearby_after_timeout">Nearby closed since it was idle.</string>
|
||||
|
||||
<string name="install_confirm">needs access to</string>
|
||||
<string name="install_confirm_update">Do you want to install an update
|
||||
|
14
app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
Normal file
14
app/src/testFull/java/org/fdroid/fdroid/data/ShadowApp.java
Normal file
@ -0,0 +1,14 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.Context;
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
|
||||
@Implements(App.class)
|
||||
public class ShadowApp extends ValueObject {
|
||||
|
||||
@Implementation
|
||||
protected static int[] getMinTargetMaxSdkVersions(Context context, String packageName) {
|
||||
return new int[]{10, 23, Apk.SDK_VERSION_MAX_VALUE};
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package org.fdroid.fdroid.localrepo;
|
||||
|
||||
import android.content.Context;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class LocalHTTPDManagerTest {
|
||||
|
||||
@Test
|
||||
public void testStartStop() throws InterruptedException {
|
||||
ShadowLog.stream = System.out;
|
||||
Context context = RuntimeEnvironment.application;
|
||||
|
||||
final String host = "localhost";
|
||||
final int port = 8888;
|
||||
assertFalse(Utils.isServerSocketInUse(port));
|
||||
LocalHTTPDManager.stop(context);
|
||||
|
||||
FDroidApp.ipAddressString = host;
|
||||
FDroidApp.port = port;
|
||||
|
||||
LocalHTTPDManager.start(context, false);
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Utils.isServerSocketInUse(port)) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
startLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
assertTrue(startLatch.await(30, TimeUnit.SECONDS));
|
||||
assertTrue(Utils.isServerSocketInUse(port));
|
||||
assertTrue(Utils.canConnectToSocket(host, port));
|
||||
|
||||
LocalHTTPDManager.stop(context);
|
||||
final CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Utils.isServerSocketInUse(port)) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
stopLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
assertTrue(stopLatch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
@ -76,11 +77,16 @@ public class LocalHTTPDTest {
|
||||
private static Thread serverStartThread;
|
||||
private static File webRoot;
|
||||
|
||||
private final int port = 38723;
|
||||
private final String baseUrl = "http://localhost:" + port;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ShadowLog.stream = System.out;
|
||||
classLoader = getClass().getClassLoader();
|
||||
|
||||
assertFalse(Utils.isServerSocketInUse(port));
|
||||
|
||||
final Context context = RuntimeEnvironment.application.getApplicationContext();
|
||||
webRoot = context.getFilesDir();
|
||||
FileUtils.deleteDirectory(webRoot);
|
||||
@ -99,7 +105,7 @@ public class LocalHTTPDTest {
|
||||
localHttpd = new LocalHTTPD(
|
||||
context,
|
||||
"localhost",
|
||||
8888,
|
||||
port,
|
||||
webRoot,
|
||||
false);
|
||||
try {
|
||||
@ -112,7 +118,9 @@ public class LocalHTTPDTest {
|
||||
});
|
||||
serverStartThread.start();
|
||||
// give the server some tine to start.
|
||||
Thread.sleep(100);
|
||||
do {
|
||||
Thread.sleep(100);
|
||||
} while (!Utils.isServerSocketInUse(port));
|
||||
}
|
||||
|
||||
@After
|
||||
@ -125,7 +133,7 @@ public class LocalHTTPDTest {
|
||||
|
||||
@Test
|
||||
public void doTest404() throws Exception {
|
||||
HttpURLConnection connection = getNoKeepAliveConnection("http://localhost:8888/xxx/yyy.html");
|
||||
HttpURLConnection connection = getNoKeepAliveConnection(baseUrl + "/xxx/yyy.html");
|
||||
connection.setReadTimeout(5000);
|
||||
connection.connect();
|
||||
Assert.assertEquals(404, connection.getResponseCode());
|
||||
@ -134,14 +142,14 @@ public class LocalHTTPDTest {
|
||||
|
||||
@Test
|
||||
public void doSomeBasicTest() throws Exception {
|
||||
URL url = new URL("http://localhost:8888/testdir/test.html");
|
||||
URL url = new URL(baseUrl + "/testdir/test.html");
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
String string = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string);
|
||||
connection.disconnect();
|
||||
|
||||
url = new URL("http://localhost:8888/");
|
||||
url = new URL(baseUrl + "/");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
string = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
@ -149,7 +157,7 @@ public class LocalHTTPDTest {
|
||||
assertTrue(string.indexOf("testdir") > 0);
|
||||
connection.disconnect();
|
||||
|
||||
url = new URL("http://localhost:8888/testdir");
|
||||
url = new URL(baseUrl + "/testdir");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
string = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
@ -158,7 +166,7 @@ public class LocalHTTPDTest {
|
||||
|
||||
IOUtils.copy(classLoader.getResourceAsStream("index.microg.jar"),
|
||||
new FileOutputStream(new File(webRoot, "index.microg.jar")));
|
||||
url = new URL("http://localhost:8888/index.microg.jar");
|
||||
url = new URL(baseUrl + "/index.microg.jar");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
byte[] actual = IOUtils.toByteArray(connection.getInputStream());
|
||||
@ -168,7 +176,7 @@ public class LocalHTTPDTest {
|
||||
|
||||
IOUtils.copy(classLoader.getResourceAsStream("extendedPerms.xml"),
|
||||
new FileOutputStream(new File(webRoot, "extendedPerms.xml")));
|
||||
url = new URL("http://localhost:8888/extendedPerms.xml");
|
||||
url = new URL(baseUrl + "/extendedPerms.xml");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
actual = IOUtils.toByteArray(connection.getInputStream());
|
||||
@ -183,7 +191,7 @@ public class LocalHTTPDTest {
|
||||
String mimeType = "application/vnd.android.package-archive";
|
||||
IOUtils.copy(classLoader.getResourceAsStream(fileName),
|
||||
new FileOutputStream(new File(webRoot, fileName)));
|
||||
URL url = new URL("http://localhost:8888/" + fileName);
|
||||
URL url = new URL(baseUrl + "/" + fileName);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("HEAD");
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
@ -197,7 +205,7 @@ public class LocalHTTPDTest {
|
||||
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
|
||||
new FileOutputStream(indexFile));
|
||||
|
||||
URL url = new URL("http://localhost:8888/");
|
||||
URL url = new URL(baseUrl + "/");
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("HEAD");
|
||||
String mimeType = "text/html";
|
||||
@ -211,12 +219,11 @@ public class LocalHTTPDTest {
|
||||
|
||||
assertEquals(200, connection.getResponseCode());
|
||||
connection.disconnect();
|
||||
Thread.sleep(100000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostRequest() throws IOException {
|
||||
URL url = new URL("http://localhost:8888/request-swap");
|
||||
URL url = new URL(baseUrl + "/request-swap");
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoInput(true);
|
||||
@ -224,7 +231,7 @@ public class LocalHTTPDTest {
|
||||
|
||||
OutputStream outputStream = connection.getOutputStream();
|
||||
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
|
||||
writer.write("repo=http://localhost:8888");
|
||||
writer.write("repo=" + baseUrl);
|
||||
writer.flush();
|
||||
writer.close();
|
||||
outputStream.close();
|
||||
@ -235,14 +242,14 @@ public class LocalHTTPDTest {
|
||||
|
||||
@Test
|
||||
public void testBadPostRequest() throws IOException {
|
||||
URL url = new URL("http://localhost:8888/request-swap");
|
||||
URL url = new URL(baseUrl + "/request-swap");
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
OutputStream outputStream = connection.getOutputStream();
|
||||
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
|
||||
writer.write("repolkasdfkjhttp://localhost:8888");
|
||||
writer.write("repolkasdfkj" + baseUrl);
|
||||
writer.flush();
|
||||
writer.close();
|
||||
outputStream.close();
|
||||
@ -294,7 +301,7 @@ public class LocalHTTPDTest {
|
||||
@Test
|
||||
public void testURLContainsParentDirectory() throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
URL url = new URL("http://localhost:8888/testdir/../index.html");
|
||||
URL url = new URL(baseUrl + "/testdir/../index.html");
|
||||
try {
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
Assert.assertEquals("The response status should be 403(Forbidden), " + "since the server won't serve requests with '../' due to security reasons",
|
||||
@ -315,7 +322,7 @@ public class LocalHTTPDTest {
|
||||
assertTrue(indexDir.mkdir());
|
||||
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
|
||||
new FileOutputStream(new File(indexDir, "index.html")));
|
||||
URL url = new URL("http://localhost:8888/" + dirName);
|
||||
URL url = new URL(baseUrl + "/" + dirName);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory," + " the server should respond with that file",
|
||||
@ -323,7 +330,7 @@ public class LocalHTTPDTest {
|
||||
|
||||
IOUtils.copy(classLoader.getResourceAsStream("index.html"),
|
||||
new FileOutputStream(new File(webRoot, "index.html")));
|
||||
url = new URL("http://localhost:8888/");
|
||||
url = new URL(baseUrl + "/");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
Assert.assertThat("When the URL ends with a directory, and if an index.html file is present in that directory,"
|
||||
@ -340,7 +347,7 @@ public class LocalHTTPDTest {
|
||||
public void testRangeHeaderWithStartPositionOnly() throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
connection = getNoKeepAliveConnection("http://localhost:8888/testdir/test.html");
|
||||
connection = getNoKeepAliveConnection(baseUrl + "/testdir/test.html");
|
||||
connection.addRequestProperty("range", "bytes=10-");
|
||||
connection.setReadTimeout(5000);
|
||||
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
@ -365,7 +372,7 @@ public class LocalHTTPDTest {
|
||||
public void testRangeStartGreaterThanFileLength() throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL("http://localhost:8888/testdir/test.html");
|
||||
URL url = new URL(baseUrl + "/testdir/test.html");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.addRequestProperty("range", "bytes=1000-");
|
||||
connection.connect();
|
||||
@ -384,7 +391,7 @@ public class LocalHTTPDTest {
|
||||
public void testRangeHeaderWithStartAndEndPosition() throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL("http://localhost:8888/testdir/test.html");
|
||||
URL url = new URL(baseUrl + "/testdir/test.html");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.addRequestProperty("range", "bytes=10-40");
|
||||
String responseString = IOUtils.toString(connection.getInputStream(), "UTF-8");
|
||||
@ -412,7 +419,7 @@ public class LocalHTTPDTest {
|
||||
while (status == -1) {
|
||||
System.out.println("testIfNoneMatchHeader connect attempt");
|
||||
try {
|
||||
connection = getNoKeepAliveConnection("http://localhost:8888/testdir/test.html");
|
||||
connection = getNoKeepAliveConnection(baseUrl + "/testdir/test.html");
|
||||
connection.setRequestProperty("if-none-match", "*");
|
||||
connection.connect();
|
||||
status = connection.getResponseCode();
|
||||
@ -430,7 +437,7 @@ public class LocalHTTPDTest {
|
||||
public void testRangeHeaderAndIfNoneMatchHeader() throws IOException {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL("http://localhost:8888/testdir/test.html");
|
||||
URL url = new URL(baseUrl + "/testdir/test.html");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.addRequestProperty("range", "bytes=10-20");
|
||||
connection.addRequestProperty("if-none-match", "*");
|
||||
|
@ -0,0 +1,187 @@
|
||||
package org.fdroid.fdroid.updater;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.IndexUpdater;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.TestUtils;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.data.ShadowApp;
|
||||
import org.fdroid.fdroid.data.TempAppProvider;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoService;
|
||||
import org.fdroid.fdroid.net.LocalHTTPD;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowContentResolver;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
import org.robolectric.shadows.ShadowPackageManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
/**
|
||||
* This test almost works, it needs to have the {@link android.content.ContentProvider}
|
||||
* and {@link ContentResolver} stuff worked out. It currently fails as
|
||||
* {@code updater.update()}.
|
||||
*/
|
||||
@Ignore
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = ShadowApp.class)
|
||||
public class SwapRepoTest {
|
||||
|
||||
private LocalHTTPD localHttpd;
|
||||
|
||||
protected ShadowContentResolver shadowContentResolver;
|
||||
protected ContentResolver contentResolver;
|
||||
protected ContextWrapper context;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ShadowLog.stream = System.out;
|
||||
|
||||
contentResolver = RuntimeEnvironment.application.getContentResolver();
|
||||
shadowContentResolver = Shadows.shadowOf(contentResolver);
|
||||
context = new ContextWrapper(RuntimeEnvironment.application.getApplicationContext()) {
|
||||
@Override
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
};
|
||||
|
||||
TestUtils.registerContentProvider(ApkProvider.getAuthority(), ApkProvider.class);
|
||||
TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class);
|
||||
TestUtils.registerContentProvider(RepoProvider.getAuthority(), RepoProvider.class);
|
||||
TestUtils.registerContentProvider(TempAppProvider.getAuthority(), TempAppProvider.class);
|
||||
|
||||
Preferences.setupForTests(context);
|
||||
}
|
||||
|
||||
@After
|
||||
public final void tearDownBase() {
|
||||
DBHelper.clearDbHelperSingleton();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.net.WifiStateChangeService.WifiInfoThread#run()
|
||||
*/
|
||||
@Test
|
||||
public void testSwap()
|
||||
throws IOException, LocalRepoKeyStore.InitException, IndexUpdater.UpdateException, InterruptedException {
|
||||
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
ShadowPackageManager shadowPackageManager = shadowOf(packageManager);
|
||||
ApplicationInfo appInfo = new ApplicationInfo();
|
||||
appInfo.flags = 0;
|
||||
appInfo.packageName = context.getPackageName();
|
||||
appInfo.minSdkVersion = 10;
|
||||
appInfo.targetSdkVersion = 23;
|
||||
appInfo.sourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
|
||||
appInfo.publicSourceDir = getClass().getClassLoader().getResource("F-Droid.apk").getPath();
|
||||
System.out.println("appInfo.sourceDir " + appInfo.sourceDir);
|
||||
appInfo.name = "F-Droid";
|
||||
|
||||
PackageInfo packageInfo = new PackageInfo();
|
||||
packageInfo.packageName = appInfo.packageName;
|
||||
packageInfo.applicationInfo = appInfo;
|
||||
packageInfo.versionCode = 1002001;
|
||||
packageInfo.versionName = "1.2-fake";
|
||||
shadowPackageManager.addPackage(packageInfo);
|
||||
|
||||
try {
|
||||
FDroidApp.initWifiSettings();
|
||||
FDroidApp.ipAddressString = "127.0.0.1";
|
||||
FDroidApp.subnetInfo = new SubnetUtils("127.0.0.0/8").getInfo();
|
||||
FDroidApp.repo.name = "test";
|
||||
FDroidApp.repo.address = "http://" + FDroidApp.ipAddressString + ":" + FDroidApp.port + "/fdroid/repo";
|
||||
|
||||
LocalRepoService.runProcess(context, new String[]{context.getPackageName()});
|
||||
File indexJarFile = LocalRepoManager.get(context).getIndexJar();
|
||||
System.out.println("indexJarFile:" + indexJarFile);
|
||||
assertTrue(indexJarFile.isFile());
|
||||
|
||||
localHttpd = new LocalHTTPD(
|
||||
context,
|
||||
FDroidApp.ipAddressString,
|
||||
FDroidApp.port,
|
||||
LocalRepoManager.get(context).getWebRoot(),
|
||||
false);
|
||||
localHttpd.start();
|
||||
Thread.sleep(100); // give the server some tine to start.
|
||||
assertTrue(localHttpd.isAlive());
|
||||
|
||||
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
|
||||
Certificate localCert = localRepoKeyStore.getCertificate();
|
||||
String signingCert = Hasher.hex(localCert);
|
||||
assertFalse(TextUtils.isEmpty(signingCert));
|
||||
assertFalse(TextUtils.isEmpty(Utils.calcFingerprint(localCert)));
|
||||
|
||||
Repo repo = MultiIndexUpdaterTest.createRepo(FDroidApp.repo.name, FDroidApp.repo.address,
|
||||
context, signingCert);
|
||||
IndexUpdater updater = new IndexUpdater(context, repo);
|
||||
updater.update();
|
||||
assertTrue(updater.hasChanged());
|
||||
updater.processDownloadedFile(indexJarFile);
|
||||
|
||||
boolean foundRepo = false;
|
||||
for (Repo repoFromDb : RepoProvider.Helper.all(context)) {
|
||||
if (TextUtils.equals(repo.address, repoFromDb.address)) {
|
||||
foundRepo = true;
|
||||
repo = repoFromDb;
|
||||
}
|
||||
}
|
||||
assertTrue(foundRepo);
|
||||
|
||||
assertNotEquals(-1, repo.getId());
|
||||
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL);
|
||||
assertEquals(1, apks.size());
|
||||
for (Apk apk : apks) {
|
||||
System.out.println(apk);
|
||||
}
|
||||
//MultiIndexUpdaterTest.assertApksExist(apks, context.getPackageName(), new int[]{BuildConfig.VERSION_CODE});
|
||||
Thread.sleep(10000);
|
||||
} finally {
|
||||
if (localHttpd != null) {
|
||||
localHttpd.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestLocalRepoService extends LocalRepoService {
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
super.onHandleIntent(intent);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,15 @@ maxlengths = {
|
||||
"menu_uninstall": 20,
|
||||
"nearby_splash__find_people_button": 30,
|
||||
"nearby_splash__request_permission": 30,
|
||||
"swap": 25,
|
||||
"swap_nfc_title": 25,
|
||||
"swap_choose_apps": 25,
|
||||
"swap_confirm": 25,
|
||||
"swap_connecting": 25,
|
||||
"swap_nearby": 25,
|
||||
"swap_scan_qr": 18,
|
||||
"swap_send_fdroid": 18,
|
||||
"swap_success": 25,
|
||||
"update_all": 20,
|
||||
"updates__hide_updateable_apps": 35,
|
||||
"updates__show_updateable_apps": 35,
|
||||
|
Loading…
x
Reference in New Issue
Block a user