Merge branch 'major-swap-overhaul' into 'master'

major swap overhaul

See merge request fdroid/fdroidclient!825
This commit is contained in:
Hans-Christoph Steiner 2019-05-24 13:34:51 +00:00
commit f69b38aad5
65 changed files with 3187 additions and 2633 deletions

View File

@ -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'

View File

@ -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 -->

View 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();
}
}
}

View File

@ -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) {
}
};
}
}

View File

@ -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();
}
};
}

View File

@ -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;
}
}

View File

@ -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) {
};
}

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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));
}
};
}

View File

@ -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;

View File

@ -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));
}
}
}

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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())
);
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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());

View File

@ -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

View File

@ -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.
}
}
}

View File

@ -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 {

View File

@ -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);
}
}
});

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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();
}
};
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}

View File

@ -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();
}
};
}

View File

@ -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);

View File

@ -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;
}
}
};
}

View File

@ -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();
}
};
}

View File

@ -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"
/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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)) {

View File

@ -47,7 +47,7 @@ public class BluetoothConnection {
Utils.closeQuietly(socket);
}
public void close() throws IOException {
public void close() {
closeQuietly();
}
}

View File

@ -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

View File

@ -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)) {

View File

@ -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"

View File

@ -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"

View File

@ -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

View 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};
}
}

View File

@ -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));
}
}

View File

@ -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", "*");

View File

@ -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);
}
}
}

View File

@ -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,