diff --git a/F-Droid/res/layout/swap_app_list_item.xml b/F-Droid/res/layout/swap_app_list_item.xml
new file mode 100644
index 000000000..a33e49557
--- /dev/null
+++ b/F-Droid/res/layout/swap_app_list_item.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/swap_blank.xml b/F-Droid/res/layout/swap_blank.xml
index 75c7c1f55..d945c89cb 100644
--- a/F-Droid/res/layout/swap_blank.xml
+++ b/F-Droid/res/layout/swap_blank.xml
@@ -7,196 +7,209 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
+ android:layout_height="130dp">
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tools:checked="true"
+ android:id="@+id/switch_bluetooth" />
-
+ android:padding="10dp"
+ android:orientation="horizontal">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:paddingStart="20dp"
+ android:paddingEnd="20dp"
+ android:paddingBottom="5dp"
+ android:paddingTop="20dp">
-
+
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:text="@string/swap_cant_find_peers"
+ android:paddingStart="20dp"
+ android:paddingTop="20dp"
+ android:textColor="@color/swap_light_text" />
-
+
+
+
+
+
+
+
diff --git a/F-Droid/res/layout/swap_confirm_receive.xml b/F-Droid/res/layout/swap_confirm_receive.xml
index 899b1af2c..9c819dd29 100644
--- a/F-Droid/res/layout/swap_confirm_receive.xml
+++ b/F-Droid/res/layout/swap_confirm_receive.xml
@@ -1,6 +1,6 @@
-
+
-
+
+ android:background="@color/swap_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
-
+
diff --git a/F-Droid/res/layout/swap_peer_list_item.xml b/F-Droid/res/layout/swap_peer_list_item.xml
index c47aaf3a6..be779ce94 100644
--- a/F-Droid/res/layout/swap_peer_list_item.xml
+++ b/F-Droid/res/layout/swap_peer_list_item.xml
@@ -3,15 +3,15 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="?attr/listPreferredItemHeight"
android:orientation="horizontal"
- android:minHeight="?android:attr/listPreferredItemHeight"
- android:paddingBottom="2dip"
- android:paddingTop="2dip">
+ android:minHeight="?attr/listPreferredItemHeight"
+ android:paddingBottom="4dip"
+ android:paddingTop="4dip">
diff --git a/F-Droid/res/layout/swap_success.xml b/F-Droid/res/layout/swap_success.xml
index e00d3c09c..dce3336f6 100644
--- a/F-Droid/res/layout/swap_success.xml
+++ b/F-Droid/res/layout/swap_success.xml
@@ -1,9 +1,9 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/F-Droid/res/menu/swap_search.xml b/F-Droid/res/menu/swap_search.xml
new file mode 100644
index 000000000..0108a7743
--- /dev/null
+++ b/F-Droid/res/menu/swap_search.xml
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml
index 5e22c6ca1..8966e549c 100644
--- a/F-Droid/res/values/strings.xml
+++ b/F-Droid/res/values/strings.xml
@@ -54,6 +54,7 @@
like
this: https://f-droid.org/repo
+ Incompatible
Installed
Not Installed
Installed (from %s)
@@ -336,6 +337,8 @@
SEND F-DROID
SCAN QR CODE
Could not find people nearby to swap with.
+ Connecting
+ Confirm swap
Public
@@ -344,5 +347,4 @@
May work
Promising
Best bet
- Connecting
diff --git a/F-Droid/src/javax/jmdns/impl/FDroidServiceInfo.java b/F-Droid/src/javax/jmdns/impl/FDroidServiceInfo.java
new file mode 100644
index 000000000..657131ce5
--- /dev/null
+++ b/F-Droid/src/javax/jmdns/impl/FDroidServiceInfo.java
@@ -0,0 +1,100 @@
+package javax.jmdns.impl;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+
+import javax.jmdns.ServiceInfo;
+
+/**
+ * The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
+ * In order to make it Parcelable (or Serializable for that matter), there are some package-scope
+ * methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
+ * it can access those methods. This is as an alternative to modifying the source code of JmDNS.
+ */
+public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
+
+ public FDroidServiceInfo(ServiceInfo info) {
+ super(info);
+ }
+
+ private static byte[] readBytes(Parcel in) {
+ byte[] bytes = new byte[in.readInt()];
+ in.readByteArray(bytes);
+ return bytes;
+ }
+
+ public FDroidServiceInfo(Parcel in) {
+ super(
+ in.readString(),
+ in.readString(),
+ in.readString(),
+ in.readInt(),
+ in.readInt(),
+ in.readInt(),
+ in.readByte() != 0,
+ readBytes(in)
+ );
+
+ int addressCount = in.readInt();
+ for (int i = 0; i < addressCount; i ++) {
+ try {
+ addAddress((Inet4Address)Inet4Address.getByAddress(readBytes(in)));
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ }
+ }
+
+ addressCount = in.readInt();
+ for (int i = 0; i < addressCount; i ++) {
+ try {
+ addAddress((Inet6Address)Inet6Address.getByAddress(readBytes(in)));
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(getType());
+ dest.writeString(getName());
+ dest.writeString(getSubtype());
+ dest.writeInt(getPort());
+ dest.writeInt(getWeight());
+ dest.writeInt(getPriority());
+ dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
+ dest.writeInt(getTextBytes().length);
+ dest.writeByteArray(getTextBytes());
+ dest.writeInt(getInet4Addresses().length);
+ for (int i = 0; i < getInet4Addresses().length; i ++) {
+ Inet4Address address = getInet4Addresses()[i];
+ dest.writeInt(address.getAddress().length);
+ dest.writeByteArray(address.getAddress());
+ }
+ dest.writeInt(getInet6Addresses().length);
+ for (int i = 0; i < getInet6Addresses().length; i ++) {
+ Inet6Address address = getInet6Addresses()[i];
+ dest.writeInt(address.getAddress().length);
+ dest.writeByteArray(address.getAddress());
+ }
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public FDroidServiceInfo createFromParcel(Parcel source) {
+ return new FDroidServiceInfo(source);
+ }
+
+ public FDroidServiceInfo[] newArray(int size) {
+ return new FDroidServiceInfo[size];
+ }
+ };
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
index 5d5f44596..4af446d9e 100644
--- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java
+++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java
@@ -40,6 +40,7 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
@@ -110,13 +111,14 @@ public class UpdateService extends IntentService implements ProgressListener {
// update in response to a user request.
public static class UpdateReceiver extends ResultReceiver {
- private Context context;
+ private final Context context;
private ProgressDialog dialog;
private ProgressListener listener;
private String lastShownMessage = null;
- public UpdateReceiver(Handler handler) {
+ public UpdateReceiver(@NonNull Context context, Handler handler) {
super(handler);
+ this.context = context;
}
public UpdateReceiver setDialog(ProgressDialog dialog) {
@@ -145,8 +147,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
}
- public UpdateReceiver showDialog(Context context) {
- this.context = context;
+ public UpdateReceiver showDialog() {
ensureDialog();
dialog.show();
return this;
@@ -227,9 +228,9 @@ public class UpdateService extends IntentService implements ProgressListener {
public static UpdateReceiver updateRepoNow(String address, Context context, boolean showDialog) {
Intent intent = new Intent(context, UpdateService.class);
- UpdateReceiver receiver = new UpdateReceiver(new Handler());
+ UpdateReceiver receiver = new UpdateReceiver(context, new Handler());
if (showDialog) {
- receiver.showDialog(context);
+ receiver.showDialog();
}
intent.putExtra(EXTRA_RECEIVER, receiver);
if (!TextUtils.isEmpty(address)) {
diff --git a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
index 5329e8d91..0f675fe96 100644
--- a/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
+++ b/F-Droid/src/org/fdroid/fdroid/data/AppProvider.java
@@ -193,7 +193,7 @@ public class AppProvider extends FDroidProvider {
}
String[] ALL = {
- IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION,
+ _ID, IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION,
LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL,
BITCOIN_ADDR, LITECOIN_ADDR, DOGECOIN_ADDR, FLATTR_ID,
UPSTREAM_VERSION, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
@@ -396,6 +396,7 @@ public class AppProvider extends FDroidProvider {
private static final String PATH_INSTALLED = "installed";
private static final String PATH_CAN_UPDATE = "canUpdate";
private static final String PATH_SEARCH = "search";
+ private static final String PATH_SEARCH_REPO = "searchRepo";
private static final String PATH_NO_APKS = "noApks";
private static final String PATH_APPS = "apps";
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
@@ -416,6 +417,7 @@ public class AppProvider extends FDroidProvider {
private static final int IGNORED = CATEGORY + 1;
private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
private static final int REPO = CALC_APP_DETAILS_FROM_INDEX + 1;
+ private static final int SEARCH_REPO = REPO + 1;
static {
matcher.addURI(getAuthority(), null, CODE_LIST);
@@ -425,6 +427,7 @@ public class AppProvider extends FDroidProvider {
matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH);
+ matcher.addURI(getAuthority(), PATH_SEARCH_REPO + "/*/*", SEARCH_REPO);
matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO);
matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
matcher.addURI(getAuthority(), PATH_INSTALLED, INSTALLED);
@@ -508,6 +511,14 @@ public class AppProvider extends FDroidProvider {
.build();
}
+ public static Uri getSearchUri(Repo repo, String query) {
+ return getContentUri().buildUpon()
+ .appendPath(PATH_SEARCH_REPO)
+ .appendPath(repo.id + "")
+ .appendPath(query)
+ .build();
+ }
+
@Override
protected String getTableName() { return DBHelper.TABLE_APP; }
@@ -680,6 +691,11 @@ public class AppProvider extends FDroidProvider {
includeSwap = false;
break;
+ case SEARCH_REPO:
+ selection = selection.add(querySearch(uri.getPathSegments().get(1)));
+ selection = selection.add(queryRepo(Long.parseLong(uri.getPathSegments().get(0))));
+ break;
+
case NO_APKS:
selection = selection.add(queryNoApks());
break;
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java
index 9b8a70a15..d89f8003e 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java
@@ -1,13 +1,14 @@
package org.fdroid.fdroid.localrepo;
-import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
+import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
@@ -16,8 +17,10 @@ import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.UpdateService;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.localrepo.peers.Peer;
import java.lang.annotation.Retention;
@@ -116,12 +119,14 @@ public class SwapManager {
// ("Step" refers to the current view being shown in the UI)
// ==========================================================
- public static final int STEP_INTRO = 1;
- public static final int STEP_SELECT_APPS = 2;
- public static final int STEP_JOIN_WIFI = 3;
- public static final int STEP_SHOW_NFC = 4;
- public static final int STEP_WIFI_QR = 5;
- public static final int STEP_CONNECTING = 6;
+ public static final int STEP_INTRO = 1;
+ public static final int STEP_SELECT_APPS = 2;
+ public static final int STEP_JOIN_WIFI = 3;
+ public static final int STEP_SHOW_NFC = 4;
+ public static final int STEP_WIFI_QR = 5;
+ public static final int STEP_CONNECTING = 6;
+ public static final int STEP_SUCCESS = 7;
+ public static final int STEP_CONFIRM_SWAP = 8;
private @SwapStep int step = STEP_INTRO;
@@ -143,13 +148,108 @@ public class SwapManager {
return appsToSwap;
}
+ @Nullable
+ public UpdateService.UpdateReceiver refreshSwap() {
+ return this.peer != null ? connectTo(peer) : null;
+ }
+
+ @NonNull
public UpdateService.UpdateReceiver 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 (!newRepoConfig.preventFurtherSwaps() && isEnabled()) {
+ askServerToSwapWithUs();
+ }*/
+
return UpdateService.updateRepoNow(peer.getRepoAddress(), context, false);
}
+/*
+ private void askServerToSwapWithUs() {
+ if (!newRepoConfig.isValidRepo()) {
+ return;
+ }
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... args) {
+ Uri repoUri = newRepoConfig.getRepoUri();
+ String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString();
+
+ AndroidHttpClient client = AndroidHttpClient.newInstance("F-Droid", ConnectSwapActivity.this);
+ HttpPost request = new HttpPost("/request-swap");
+ HttpHost host = new HttpHost(repoUri.getHost(), repoUri.getPort(), repoUri.getScheme());
+
+ try {
+ Log.d(TAG, "Asking server at " + newRepoConfig.getRepoUriString() + " to swap with us in return (by POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\")...");
+ populatePostParams(swapBackUri, request);
+ client.execute(host, request);
+ } catch (IOException e) {
+ notifyOfErrorOnUiThread();
+ Log.e(TAG, "Error while asking server to swap with us: " + e.getMessage());
+ } finally {
+ client.close();
+ }
+ return null;
+ }
+
+ private void populatePostParams(String swapBackUri, HttpPost request) throws UnsupportedEncodingException {
+ List params = new ArrayList<>();
+ params.add(new BasicNameValuePair("repo", swapBackUri));
+ UrlEncodedFormEntity encodedParams = new UrlEncodedFormEntity(params);
+ request.setEntity(encodedParams);
+ }
+
+ private void notifyOfErrorOnUiThread() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(
+ ConnectSwapActivity.this,
+ R.string.swap_reciprocate_failed,
+ Toast.LENGTH_LONG
+ ).show();
+ }
+ });
+ }
+ }.execute();
+ }*/
+ private Repo ensureRepoExists(@NonNull Peer peer) {
+ // TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with
+ // the repos address in the database. Not sure on best behaviour in this situation.
+ Repo repo = RepoProvider.Helper.findByAddress(context, peer.getRepoAddress());
+ if (repo == null) {
+ ContentValues values = new ContentValues(6);
+
+ // TODO: i18n and think about most appropriate name. Although it wont be visible in
+ // the "Manage repos" UI after being marked as a swap repo here...
+ values.put(RepoProvider.DataColumns.NAME, peer.getName());
+ values.put(RepoProvider.DataColumns.ADDRESS, peer.getRepoAddress());
+ values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO;
+ values.put(RepoProvider.DataColumns.FINGERPRINT, peer.getFingerprint());
+ values.put(RepoProvider.DataColumns.IN_USE, true);
+ values.put(RepoProvider.DataColumns.IS_SWAP, true);
+ Uri uri = RepoProvider.Helper.insert(context, values);
+ repo = RepoProvider.Helper.findByUri(context, uri);
+ }
+
+ return repo;
+ }
+
+ @Nullable
+ public Repo getPeerRepo() {
+ return peerRepo;
+ }
+
+ public void install(@NonNull final App app) {
+
+ }
/**
* Ensure that we don't get put into an incorrect state, by forcing people to pass valid
@@ -158,7 +258,7 @@ public class SwapManager {
* This is the same as, e.g. {@link Context#getSystemService(String)}
*/
@IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR,
- STEP_CONNECTING})
+ STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP})
@Retention(RetentionPolicy.SOURCE)
public @interface SwapStep {}
@@ -171,6 +271,9 @@ public class SwapManager {
@Nullable
private Peer peer;
+ @Nullable
+ private Repo peerRepo;
+
public void swapWith(Peer peer) {
this.peer = peer;
}
@@ -313,43 +416,19 @@ public class SwapManager {
// Interacting with Bluetooth adapter
// ==========================================
- @Nullable /* Emulators tend not to have bluetooth adapters. */
- private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
-
public boolean isBluetoothDiscoverable() {
- return bluetooth != null &&
- bluetooth.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
+ return service != null && service.getBluetooth().isConnected();
}
public void ensureBluetoothDiscoverable() {
- if (bluetooth == null) {
- return;
- }
-
- if (!bluetooth.isEnabled()) {
- if (!bluetooth.enable()) {
-
- }
- }
-
- if (bluetooth.isEnabled()) {
- Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
- discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
-
- // TODO: Hmm, don't like the idea of a background service being able to do this :(
- discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- context.startActivity(discoverableIntent);
+ if (service != null) {
+ service.getBluetooth().start();
}
}
public void makeBluetoothNonDiscoverable() {
- if (bluetooth == null) {
- return;
- }
-
- if (isBluetoothDiscoverable()) {
- // TODO: How to disable this?
+ if (service != null) {
+ service.getBluetooth().stop();
}
}
@@ -361,6 +440,18 @@ public class SwapManager {
return isWifiConnected() && service != null && service.isEnabled();
}
+ public void ensureBonjourDiscoverable() {
+ if (!isBonjourDiscoverable()) {
+ // TODO: Enable bonjour (currently it is enabled by default when the service starts)
+ }
+ }
+
+ public void makeBonjourNotDiscoverable() {
+ if (service != null) {
+ // TODO: Disable bonjour (currently it is enabled by default when the service starts)
+ }
+ }
+
public boolean isScanningForPeers() {
return service != null && service.isScanningForPeers();
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
index 845fefd74..ab0a761c7 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java
@@ -18,8 +18,9 @@ import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.peers.BluetoothFinder;
import org.fdroid.fdroid.localrepo.peers.BonjourFinder;
+import org.fdroid.fdroid.localrepo.type.BluetoothType;
import org.fdroid.fdroid.localrepo.type.BonjourType;
-import org.fdroid.fdroid.localrepo.type.NfcType;
+import org.fdroid.fdroid.localrepo.type.SwapType;
import org.fdroid.fdroid.localrepo.type.WebServerType;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
@@ -31,27 +32,26 @@ import java.util.TimerTask;
* Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps. Currently manages WiFi and NFC. Will manage Bluetooth in
* the future.
- *
- * TODO: Manage threading correctly.
*/
public class SwapService extends Service {
private static final String TAG = "SwapService";
+ 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 EXTRA_STARTING = "STARTING";
+ public static final String EXTRA_STARTED = "STARTED";
+ public static final String EXTRA_STOPPED = "STOPPED";
+
private static final int NOTIFICATION = 1;
private final Binder binder = new Binder();
- private final BonjourType bonjourType;
- private final WebServerType webServerType;
+ private SwapType bonjourType;
+ private SwapType bluetoothType;
+ private SwapType webServerType;
- private final BonjourFinder bonjourFinder;
- private final BluetoothFinder bluetoothFinder;
-
-
- // TODO: The NFC type can't really be managed by the service, because it is intrinsically tied
- // to a specific _Activity_, and will only be active while that activity is shown. This service
- // knows nothing about activities.
- private final NfcType nfcType;
+ private BonjourFinder bonjourFinder;
+ private BluetoothFinder bluetoothFinder;
private final static int TIMEOUT = 900000; // 15 mins
@@ -65,22 +65,29 @@ public class SwapService extends Service {
return bonjourFinder.isScanning() || bluetoothFinder.isScanning();
}
+ public SwapType getBluetooth() {
+ return bluetoothType;
+ }
+
+ public SwapType getBonjour() {
+ return bluetoothType;
+ }
+
public class Binder extends android.os.Binder {
public SwapService getService() {
return SwapService.this;
}
}
- public SwapService() {
- nfcType = new NfcType(this);
+ public void onCreate() {
+ super.onCreate();
+
bonjourType = new BonjourType(this);
+ bluetoothType = BluetoothType.create(this);
webServerType = new WebServerType(this);
bonjourFinder = new BonjourFinder(this);
bluetoothFinder = new BluetoothFinder(this);
- }
- public void onCreate() {
- super.onCreate();
Log.d(TAG, "Creating service, will register appropriate listeners.");
Preferences.get().registerLocalRepoBonjourListeners(bonjourEnabledListener);
Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener);
@@ -139,7 +146,7 @@ public class SwapService extends Service {
@Override
protected Void doInBackground(Void... params) {
Log.d(TAG, "Started background task to enable swapping.");
- enableSwappingSynchronous();
+ enableSwappingAsynchronous();
return null;
}
@@ -160,13 +167,13 @@ public class SwapService extends Service {
/**
* The guts of this class - responsible for enabling the relevant services for swapping.
- * * Doesn't know anything about enabled/disabled.
- * * Runs synchronously on the thread it was called.
+ * Doesn't know anything about enabled/disabled state, you should check that before invoking
+ * this method so it doesn't start something that is already started.
+ * Runs asynchronously on several background threads.
*/
- private void enableSwappingSynchronous() {
- nfcType.start();
- webServerType.start();
- bonjourType.start();
+ private void enableSwappingAsynchronous() {
+ webServerType.startInBackground();
+ bonjourType.startInBackground();
}
public void disableSwapping() {
@@ -199,13 +206,12 @@ public class SwapService extends Service {
}
/**
- * @see SwapService#enableSwappingSynchronous()
+ * @see SwapService#enableSwappingAsynchronous()
*/
private void disableSwappingSynchronous() {
Log.d(TAG, "Disabling SwapService (bonjour, webserver, etc)");
bonjourType.stop();
webServerType.stop();
- nfcType.stop();
}
public boolean isEnabled() {
@@ -219,7 +225,7 @@ public class SwapService extends Service {
protected Void doInBackground(Void... params) {
Log.d(TAG, "Restarting swap services.");
disableSwappingSynchronous();
- enableSwappingSynchronous();
+ enableSwappingAsynchronous();
return null;
}
}.execute();
@@ -245,6 +251,7 @@ public class SwapService extends Service {
}
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
+ // TODO: Remove this preference...
private final Preferences.ChangeListener bonjourEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
index 096e81b62..2cee166f5 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BluetoothPeer.java
@@ -30,7 +30,7 @@ public class BluetoothPeer implements Peer {
}
@Override
- public boolean equals(Peer peer) {
+ public boolean equals(Object peer) {
return peer != null && peer instanceof BluetoothPeer && ((BluetoothPeer)peer).device.getAddress().equals(device.getAddress());
}
@@ -39,6 +39,12 @@ public class BluetoothPeer implements Peer {
return "bluetooth://" + device.getAddress() + "/fdroid/repo";
}
+ @Override
+ public String getFingerprint() {
+ // TODO: Get fingerprint somehow over bluetooth.
+ return "";
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
index cfffddc11..a6ad807e5 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
@@ -5,12 +5,14 @@ import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.util.Log;
+import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.FDroidApp;
-import org.fdroid.fdroid.Utils;
-import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
import java.io.IOException;
+import java.net.Inet4Address;
import java.net.InetAddress;
+import java.util.Timer;
+import java.util.TimerTask;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
@@ -24,7 +26,7 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
- private JmDNS mJmdns;
+ private JmDNS jmdns;
private WifiManager wifiManager;
private WifiManager.MulticastLock mMulticastLock;
@@ -35,6 +37,8 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
@Override
public void scan() {
+ Log.d(TAG, "Requested Bonjour (mDNS) scan for peers.");
+
if (wifiManager == null) {
wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(context.getPackageName());
@@ -42,7 +46,8 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
}
if (isScanning) {
- Log.d(TAG, "Requested Bonjour scan, but already scanning, so will ignore request.");
+ Log.d(TAG, "Requested Bonjour scan, but already scanning. But we will still try to explicitly scan for services.");
+ // listServices();
return;
}
@@ -53,16 +58,8 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
@Override
protected Void doInBackground(Void... params) {
try {
- int ip = wifiManager.getConnectionInfo().getIpAddress();
- byte[] byteIp = {
- (byte) (ip & 0xff),
- (byte) (ip >> 8 & 0xff),
- (byte) (ip >> 16 & 0xff),
- (byte) (ip >> 24 & 0xff)
- };
- Log.d(TAG, "Searching for mDNS clients...");
- mJmdns = JmDNS.create(InetAddress.getByAddress(byteIp));
- Log.d(TAG, "Finished searching for mDNS clients.");
+ Log.d(TAG, "Searching for Bonjour (mDNS) clients...");
+ jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
} catch (IOException e) {
e.printStackTrace();
}
@@ -71,16 +68,44 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
@Override
protected void onPostExecute(Void result) {
- Log.d(TAG, "Adding mDNS service listeners.");
- if (mJmdns != null) {
- mJmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this);
- mJmdns.addServiceListener(HTTPS_SERVICE_TYPE, BonjourFinder.this);
+ if (jmdns != null) {
+ Log.d(TAG, "Adding mDNS service listeners for " + HTTP_SERVICE_TYPE + " and " + HTTPS_SERVICE_TYPE);
+ jmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this);
+ jmdns.addServiceListener(HTTPS_SERVICE_TYPE, BonjourFinder.this);
+ listServices();
}
}
}.execute();
}
+ private void listServices() {
+ new AsyncTask() {
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ Log.d(TAG, "Explicitly querying for services, in addition to waiting for notifications.");
+ addFDroidServices(jmdns.list(HTTP_SERVICE_TYPE));
+ addFDroidServices(jmdns.list(HTTPS_SERVICE_TYPE));
+ return null;
+ }
+
+ // TODO: Remove once stable, added here for testing because it is easier to see the
+ // data being broadcast over mDNS.
+ /*@Override
+ protected void onPostExecute(Void v) {
+ Log.d(TAG, "Queuing up another poll in 2 secs.");
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Log.d(TAG, "Time to poll for services again.");
+ listServices();
+ }
+ }, 2000);
+ }*/
+ }.execute();
+ }
+
@Override
public void serviceRemoved(ServiceEvent event) {
}
@@ -95,11 +120,11 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
// so that more detailed info can be shown to the user.
//
// If so, when is the old one removed?
- addFDroidService(event);
+ addFDroidService(event.getInfo());
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
- mJmdns.requestServiceInfo(event.getType(), event.getName(), true);
+ jmdns.requestServiceInfo(event.getType(), event.getName(), true);
return null;
}
}.execute();
@@ -107,7 +132,13 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
@Override
public void serviceResolved(ServiceEvent event) {
- addFDroidService(event);
+ addFDroidService(event.getInfo());
+ }
+
+ private void addFDroidServices(ServiceInfo[] services) {
+ for (ServiceInfo info : services) {
+ addFDroidService(info);
+ }
}
/**
@@ -115,14 +146,24 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
* 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(ServiceEvent event) {
- final ServiceInfo serviceInfo = event.getInfo();
+ 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) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Found F-Droid swap Bonjour service:\n" + serviceInfo);
+ }
foundPeer(new BonjourPeer(serviceInfo));
+ } else {
+ if (BuildConfig.DEBUG) {
+ if (isSelf) {
+ Log.d(TAG, "Ignoring Bonjour service because it belongs to this device:\n" + serviceInfo);
+ } else {
+ Log.d(TAG, "Ignoring Bonjour service because it doesn't look like an F-Droid swap repo:\n" + serviceInfo);
+ }
+ }
}
}
@@ -132,13 +173,14 @@ public class BonjourFinder extends PeerFinder implements ServiceLis
mMulticastLock.release();
}
- if (mJmdns == null)
- return;
- mJmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
- mJmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
- mJmdns = null;
isScanning = false;
+ if (jmdns == null)
+ return;
+ jmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
+ jmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
+ jmdns = null;
+
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
index 8f0e5b91c..3418a4fe3 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/BonjourPeer.java
@@ -4,14 +4,19 @@ import android.os.Parcel;
import org.fdroid.fdroid.R;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+
+import javax.jmdns.impl.FDroidServiceInfo;
import javax.jmdns.ServiceInfo;
+import javax.jmdns.impl.ServiceInfoImpl;
public class BonjourPeer implements Peer {
- private ServiceInfo serviceInfo;
+ private FDroidServiceInfo serviceInfo;
public BonjourPeer(ServiceInfo serviceInfo) {
- this.serviceInfo = serviceInfo;
+ this.serviceInfo = new FDroidServiceInfo(serviceInfo);
}
@Override
@@ -30,18 +35,22 @@ public class BonjourPeer implements Peer {
}
@Override
- public boolean equals(Peer peer) {
+ public boolean equals(Object peer) {
if (peer != null && peer instanceof BonjourPeer) {
BonjourPeer that = (BonjourPeer)peer;
- // TODO: Don't use "name" for comparing, but rather fingerprint of the swap repo.
- return that.serviceInfo.getName().equals(this.serviceInfo.getName());
+ return this.getFingerprint().equals(that.getFingerprint());
}
return false;
}
@Override
public String getRepoAddress() {
- return serviceInfo.getURL();
+ return serviceInfo.getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
+ }
+
+ @Override
+ public String getFingerprint() {
+ return serviceInfo.getPropertyString("fingerprint");
}
@Override
@@ -51,27 +60,11 @@ public class BonjourPeer implements Peer {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(serviceInfo.getType());
- dest.writeString(serviceInfo.getName());
- dest.writeString(serviceInfo.getSubtype());
- dest.writeInt(serviceInfo.getPort());
- dest.writeInt(serviceInfo.getWeight());
- dest.writeInt(serviceInfo.getPriority());
- dest.writeByte(serviceInfo.isPersistent() ? (byte) 1 : (byte) 0);
- dest.writeString(serviceInfo.getTextString());
+ dest.writeParcelable(serviceInfo, flags);
}
protected BonjourPeer(Parcel in) {
- String type = in.readString();
- String name = in.readString();
- String subtype = in.readString();
- int port = in.readInt();
- int weight = in.readInt();
- int priority = in.readInt();
- boolean persistent = in.readByte() != 0;
- String text = in.readString();
-
- this.serviceInfo = ServiceInfo.create(type, name, subtype, port, weight, priority, persistent, text);
+ this.serviceInfo = in.readParcelable(FDroidServiceInfo.class.getClassLoader());
}
public static final Creator CREATOR = new Creator() {
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/Peer.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/Peer.java
index 490837bc4..022aaf282 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/Peer.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/Peer.java
@@ -9,7 +9,9 @@ public interface Peer extends Parcelable {
@DrawableRes int getIcon();
- boolean equals(Peer peer);
+ boolean equals(Object peer);
String getRepoAddress();
+
+ String getFingerprint();
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java
index 34915b270..452bda7c2 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java
@@ -37,4 +37,8 @@ public abstract class PeerFinder {
context.sendBroadcast(intent);
}
+ protected void removePeer(T peer) {
+ // TODO: Broadcast messages when peers are removed too.
+ }
+
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothType.java
new file mode 100644
index 000000000..75681af1a
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BluetoothType.java
@@ -0,0 +1,102 @@
+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.util.Log;
+
+import org.fdroid.fdroid.localrepo.SwapService;
+
+public class BluetoothType extends SwapType {
+
+ private static final String TAG = "BluetoothBroadcastType";
+
+ @NonNull
+ private final BluetoothAdapter adapter;
+
+ public static SwapType create(@NonNull Context context) {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ return new NoBluetoothType(context);
+ } else {
+ return new BluetoothType(context, adapter);
+ }
+ };
+
+ private BluetoothType(@NonNull Context context, @NonNull BluetoothAdapter adapter) {
+ super(context);
+ this.adapter = adapter;
+
+ context.registerReceiver(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:
+ setConnected(true);
+ break;
+
+ // Only other is BluetoothAdapter.SCAN_MODE_CONNECTABLE. For now don't handle that.
+ }
+ }
+ }, new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED));
+ }
+
+ @Override
+ public void start() {
+ sendBroadcast(SwapService.EXTRA_STARTING);
+
+ if (!adapter.isEnabled()) {
+ if (!adapter.enable()) {
+ setConnected(false);
+ return;
+ }
+ }
+
+ if (adapter.isEnabled()) {
+ Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
+ discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
+
+ // TODO: Hmm, don't like the idea of a background service being able to do this :(
+ discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // Can't get notified if they cancel this, because we are not an activity and thus
+ // can't start an activity for a result :(
+ context.startActivity(discoverableIntent);
+
+ // Don't setConnected(true) yet, wait for the broadcast to come from the BluetoothAdapter
+ // saying its state has changed.
+ }
+ }
+
+ @Override
+ public void stop() {
+ Log.d(TAG, "Making bluetooth non-discoverable.");
+ adapter.cancelDiscovery();
+ setConnected(false);
+ }
+
+ @Override
+ public String getBroadcastAction() {
+ return SwapService.BLUETOOTH_STATE_CHANGE;
+ }
+
+ private static class NoBluetoothType extends SwapType {
+
+ public NoBluetoothType(@NonNull Context context) {
+ super(context);
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void stop() {}
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java
index d047b8aaa..18a4b4e71 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java
@@ -3,33 +3,44 @@ package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
import android.util.Log;
+import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.localrepo.SwapService;
import java.io.IOException;
+import java.net.InetAddress;
import java.util.HashMap;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
-public class BonjourType implements SwapType {
+/**
+ * Sends a {@link SwapService#BONJOUR_STATE_CHANGE} broadcasts when starting, started or stopped.
+ */
+public class BonjourType extends SwapType {
- private static final String TAG = "BonjourBroadcastType";
+ private static final String TAG = "BonjourSwapService";
private JmDNS jmdns;
private ServiceInfo pairService;
- private final Context context;
public BonjourType(Context context) {
- this.context = context;
+ super(context);
}
@Override
public void start() {
- if (!Preferences.get().isLocalRepoBonjourEnabled())
+ Log.d(TAG, "Preparing to start Bonjour service.");
+ if (!Preferences.get().isLocalRepoBonjourEnabled()) {
+ // TODO: Remove this preference completely, it is now dealt with in the start swap screen.
+ Log.i(TAG, "Not going to start Bonjour swap service because disabled via preferences.");
return;
+ }
+
+ sendBroadcast(SwapService.EXTRA_STARTING);
/*
* a ServiceInfo can only be registered with a single instance
@@ -54,12 +65,14 @@ public class BonjourType implements SwapType {
try {
Log.d(TAG, "Starting bonjour service...");
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
- jmdns = JmDNS.create();
+ jmdns = JmDNS.create(InetAddress.getByName(FDroidApp.ipAddressString));
jmdns.registerService(pairService);
+ setConnected(true);
Log.d(TAG, "... Bounjour service started.");
} catch (IOException e) {
Log.e(TAG, "Error while registering jmdns service: " + e);
Log.e(TAG, Log.getStackTraceString(e));
+ setConnected(false);
}
}
@@ -67,6 +80,7 @@ public class BonjourType implements SwapType {
public void stop() {
Log.d(TAG, "Unregistering MDNS service...");
clearCurrentMDNSService();
+ setConnected(false);
}
private void clearCurrentMDNSService() {
@@ -82,8 +96,8 @@ public class BonjourType implements SwapType {
}
@Override
- public void restart() {
-
+ public String getBroadcastAction() {
+ return SwapService.BONJOUR_STATE_CHANGE;
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java
deleted file mode 100644
index f71130300..000000000
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.fdroid.fdroid.localrepo.type;
-
-import android.content.Context;
-
-public class NfcType implements SwapType {
-
- private final Context context;
-
- public NfcType(Context context) {
- this.context = context;
- }
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
-
- }
-
- @Override
- public void restart() {
-
- }
-
-}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java
index 68161616a..cabcbc660 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java
@@ -1,11 +1,73 @@
package org.fdroid.fdroid.localrepo.type;
-public interface SwapType {
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v4.content.LocalBroadcastManager;
- void start();
+import org.fdroid.fdroid.localrepo.SwapService;
- void stop();
+/**
+ * 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 {
- void restart();
+ private boolean isConnected;
+ @NonNull
+ protected final Context context;
+
+ public SwapType(@NonNull Context context) {
+ this.context = context;
+ }
+
+ abstract public void start();
+
+ abstract public void stop();
+
+ protected String getBroadcastAction() {
+ return null;
+ }
+
+ protected final void setConnected(boolean connected) {
+ if (connected) {
+ isConnected = true;
+ sendBroadcast(SwapService.EXTRA_STARTED);
+ } else {
+ isConnected = false;
+ sendBroadcast(SwapService.EXTRA_STOPPED);
+ }
+ }
+
+ /**
+ * 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);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+ }
+
+ public final boolean isConnected() {
+ return isConnected;
+ }
+
+ public void startInBackground() {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ start();
+ return null;
+ }
+ }.execute();
+ }
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java
index 9d168c98e..3131bdee1 100644
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java
+++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java
@@ -17,21 +17,21 @@ import java.io.IOException;
import java.net.BindException;
import java.util.Random;
-public class WebServerType implements SwapType {
+public class WebServerType extends SwapType {
private static final String TAG = "WebServerType";
private Handler webServerThreadHandler = null;
private LocalHTTPD localHttpd;
- private final Context context;
public WebServerType(Context context) {
- this.context = context;
+ super(context);
}
@Override
public void start() {
+ Log.d(TAG, "Preparing swap webserver.");
Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@@ -51,7 +51,9 @@ public class WebServerType implements SwapType {
}
};
try {
+ Log.d(TAG, "Starting swap webserver...");
localHttpd.start();
+ Log.d(TAG, "Swap webserver started.");
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
@@ -79,9 +81,4 @@ public class WebServerType implements SwapType {
webServerThreadHandler.sendMessage(msg);
}
- @Override
- public void restart() {
-
- }
-
}
diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java
deleted file mode 100644
index 2ba29851c..000000000
--- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.fdroid.fdroid.localrepo.type;
-
-import android.content.Context;
-
-public class WifiType implements SwapType {
-
- private final Context context;
-
- public WifiType(Context context) {
- this.context = context;
- }
-
- @Override
- public void start() {
-
- }
-
- @Override
- public void stop() {
-
- }
-
- @Override
- public void restart() {
-
- }
-
-}
diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
index fa55b8dbe..3b5171a26 100644
--- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
+++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java
@@ -38,6 +38,7 @@ public class WifiStateChangeService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again.");
FDroidApp.initWifiSettings();
NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
@@ -66,6 +67,7 @@ public class WifiStateChangeService extends Service {
@Override
protected Void doInBackground(Void... params) {
try {
+ Log.d(TAG, "Checking wifi state (in background thread).");
WifiInfo wifiInfo = null;
wifiState = wifiManager.getWifiState();
@@ -85,9 +87,12 @@ public class WifiStateChangeService extends Service {
} else { // a hotspot can be active during WIFI_STATE_UNKNOWN
FDroidApp.ipAddressString = getIpAddressFromNetworkInterface();
}
- Thread.sleep(1000);
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "waiting for an IP address...");
+
+ if (FDroidApp.ipAddressString == null) {
+ Thread.sleep(1000);
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "waiting for an IP address...");
+ }
}
}
if (isCancelled()) // can be canceled by a change via WifiStateChangeReceiver
@@ -95,6 +100,7 @@ public class WifiStateChangeService extends Service {
if (wifiInfo != null) {
String ssid = wifiInfo.getSSID();
+ Log.d(TAG, "Have wifi info, connected to " + ssid);
if (ssid != null) {
FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
}
@@ -104,6 +110,7 @@ public class WifiStateChangeService extends Service {
}
}
+ // TODO: Can this be moved to the swap service instead?
String scheme;
if (Preferences.get().isLocalRepoHttpsEnabled())
scheme = "https";
diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
index 4c2407c55..d4796f96c 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java
@@ -148,7 +148,7 @@ public class ManageReposActivity extends ActionBarActivity {
protected void onResume() {
super.onResume();
if (updateHandler != null) {
- updateHandler.showDialog(this);
+ updateHandler.showDialog();
}
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());
diff --git a/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java b/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
index 19f8990a7..04876d8d2 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/fragments/RepoDetailsFragment.java
@@ -82,7 +82,7 @@ public class RepoDetailsFragment extends Fragment {
public void onAttach(Activity activity) {
super.onAttach(activity);
if (updateHandler != null) {
- updateHandler.showDialog(getActivity());
+ updateHandler.showDialog();
}
}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java
new file mode 100644
index 000000000..15382b20b
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java
@@ -0,0 +1,79 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.v4.view.MenuItemCompat;
+import android.util.AttributeSet;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.RelativeLayout;
+
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.localrepo.SwapManager;
+
+public class ConfirmReceive extends RelativeLayout implements SwapWorkflowActivity.InnerView {
+
+ public ConfirmReceive(Context context) {
+ super(context);
+ }
+
+ public ConfirmReceive(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ConfirmReceive(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ConfirmReceive(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private SwapWorkflowActivity getActivity() {
+ return (SwapWorkflowActivity)getContext();
+ }
+
+ private SwapManager getManager() {
+ return getActivity().getState();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+
+ }
+
+ @Override
+ public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
+ return true;
+ }
+
+ @Override
+ public int getStep() {
+ return SwapManager.STEP_CONFIRM_SWAP;
+ }
+
+ @Override
+ public int getPreviousStep() {
+ return SwapManager.STEP_INTRO;
+ }
+
+ @ColorRes
+ public int getToolbarColour() {
+ return getResources().getColor(R.color.swap_blue);
+ }
+
+ @Override
+ public String getToolbarTitle() {
+ return getResources().getString(R.string.swap_confirm);
+ }
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java
index 7a46c40a8..abd1a32db 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java
@@ -12,6 +12,7 @@ import android.os.Build;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -28,17 +29,20 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
+import android.widget.ScrollView;
import android.widget.Switch;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapManager;
+import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
+import org.fdroid.fdroid.net.WifiStateChangeService;
import java.util.ArrayList;
-public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView {
+public class StartSwapView extends ScrollView implements SwapWorkflowActivity.InnerView {
private static final String TAG = "StartSwapView";
@@ -111,12 +115,23 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
protected void onFinishInflate() {
super.onFinishInflate();
+ getManager().scanForPeers();
+
uiInitPeers();
uiInitBluetooth();
uiInitWifi();
uiInitButtons();
uiUpdatePeersInfo();
+ LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ uiUpdateWifi();
+ }
+ },
+ new IntentFilter(WifiStateChangeService.BROADCAST)
+ );
}
private void uiInitButtons() {
@@ -161,9 +176,13 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
@Override
public void onReceive(Context context, Intent intent) {
Peer peer = intent.getParcelableExtra(SwapManager.EXTRA_PEER);
- Log.d(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
- adapter.add(peer);
- uiUpdatePeersInfo();
+ if (adapter.getPosition(peer) >= 0) {
+ Log.d(TAG, "Found peer: " + peer + ", ignoring though, because it is already in our list.");
+ } else {
+ Log.d(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
+ adapter.add(peer);
+ uiUpdatePeersInfo();
+ }
}
}, new IntentFilter(SwapManager.ACTION_PEER_FOUND));
@@ -191,29 +210,51 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
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);
- Switch bluetoothSwitch = ((Switch) findViewById(R.id.switch_bluetooth));
+ final Switch bluetoothSwitch = ((Switch) findViewById(R.id.switch_bluetooth));
bluetoothSwitch.setChecked(getManager().isBluetoothDiscoverable());
bluetoothSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
getManager().ensureBluetoothDiscoverable();
- getManager().scanForPeers();
textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
+ viewBluetoothId.setVisibility(View.VISIBLE);
uiUpdatePeersInfo();
// TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
} else {
- getManager().cancelScanningForPeers();
getManager().makeBluetoothNonDiscoverable();
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
+ viewBluetoothId.setVisibility(View.GONE);
uiUpdatePeersInfo();
}
}
});
+
+ LocalBroadcastManager.getInstance(getContext()).registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
+ Log.d(TAG, "Bluetooth service is starting...");
+ bluetoothSwitch.setEnabled(false);
+ bluetoothSwitch.setChecked(true);
+ } else {
+ bluetoothSwitch.setEnabled(true);
+ if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
+ Log.d(TAG, "Bluetooth service has started.");
+ bluetoothSwitch.setChecked(true);
+ } else {
+ Log.d(TAG, "Bluetooth service has stopped.");
+ bluetoothSwitch.setChecked(false);
+ }
+ }
+ }
+ }, new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE));
+
} else {
findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
}
@@ -221,34 +262,56 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
private void uiInitWifi() {
- final TextView textWifiVisible = (TextView)findViewById(R.id.wifi_visible);
-
viewWifiId = (TextView)findViewById(R.id.device_id_wifi);
viewWifiNetwork = (TextView)findViewById(R.id.wifi_network);
- int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
- textWifiVisible.setText(textResource);
-
- Switch wifiSwitch = (Switch)findViewById(R.id.switch_wifi);
+ final Switch wifiSwitch = (Switch)findViewById(R.id.switch_wifi);
wifiSwitch.setChecked(getManager().isBonjourDiscoverable());
wifiSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
- textWifiVisible.setText(R.string.swap_visible_wifi);
- uiUpdatePeersInfo();
+ getManager().ensureBonjourDiscoverable();
} else {
- textWifiVisible.setText(R.string.swap_not_visible_wifi);
- uiUpdatePeersInfo();
+ getManager().makeBonjourNotDiscoverable();
}
+ uiUpdatePeersInfo();
+ uiUpdateWifi();
}
});
+ LocalBroadcastManager.getInstance(getContext()).registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
+ Log.d(TAG, "Bonjour service is starting...");
+ wifiSwitch.setEnabled(false);
+ wifiSwitch.setChecked(true);
+ } else {
+ wifiSwitch.setEnabled(true);
+ if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
+ Log.d(TAG, "Bonjour service has started.");
+ wifiSwitch.setChecked(true);
+ } else {
+ Log.d(TAG, "Bonjour service has stopped.");
+ wifiSwitch.setChecked(false);
+ }
+ }
+ uiUpdateWifi();
+ }
+ }, new IntentFilter(SwapService.BONJOUR_STATE_CHANGE));
+
uiUpdateWifi();
}
private void uiUpdateWifi() {
+
+ final TextView 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);
+
viewWifiId.setText(FDroidApp.ipAddressString);
+ viewWifiId.setVisibility(TextUtils.isEmpty(FDroidApp.ipAddressString) ? View.GONE : View.VISIBLE);
if (TextUtils.isEmpty(FDroidApp.bssid) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) {
// empty bssid with an ipAddress means hotspot mode
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java
new file mode 100644
index 000000000..fc7089c8c
--- /dev/null
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java
@@ -0,0 +1,327 @@
+package org.fdroid.fdroid.views.swap;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v4.widget.CursorAdapter;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.fdroid.fdroid.ProgressListener;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.UpdateService;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.Repo;
+import org.fdroid.fdroid.localrepo.SwapManager;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class SwapAppsView extends ListView implements
+ SwapWorkflowActivity.InnerView,
+ LoaderManager.LoaderCallbacks,
+ SearchView.OnQueryTextListener {
+
+ public SwapAppsView(Context context) {
+ super(context);
+ }
+
+ public SwapAppsView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SwapAppsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private SwapWorkflowActivity getActivity() {
+ return (SwapWorkflowActivity)getContext();
+ }
+
+ private SwapManager getState() {
+ return getActivity().getState();
+ }
+
+ private static final int LOADER_SWAPABLE_APPS = 759283741;
+ private static final String TAG = "SwapAppsView";
+
+ private Repo repo;
+ private AppListAdapter adapter;
+ private String mCurrentFilterString;
+
+ @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?
+ }
+
+ adapter = new AppListAdapter(getContext(), getContext().getContentResolver().query(
+ AppProvider.getRepoUri(repo), AppProvider.DataColumns.ALL, null, null, null));
+
+ setAdapter(adapter);
+
+ // either reconnect with an existing loader or start a new one
+ getActivity().getSupportLoaderManager().initLoader(LOADER_SWAPABLE_APPS, null, this);
+
+ setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ showAppDetails(position);
+ }
+ });
+
+ schedulePollForUpdates();
+ }
+
+ private void pollForUpdates() {
+ if (adapter.getCount() > 1 ||
+ (adapter.getCount() == 1 && !new App((Cursor)adapter.getItem(0)).id.equals("org.fdroid.fdroid"))) {
+ Log.d(TAG, "Not polling for new apps from swap repo, because we already have more than one.");
+ return;
+ }
+
+ Log.d(TAG, "Polling swap repo to see if it has any updates.");
+ UpdateService.UpdateReceiver receiver = getState().refreshSwap();
+ if (receiver != null) {
+ receiver.setListener(new ProgressListener() {
+ @Override
+ public void onProgress(Event event) {
+ switch (event.type) {
+ case UpdateService.EVENT_COMPLETE_WITH_CHANGES:
+ Log.d(TAG, "Swap repo has updates, notifying the list adapter.");
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.notifyDataSetChanged();
+ }
+ });
+ break;
+
+ case UpdateService.EVENT_ERROR:
+ // TODO: Well, if we can't get the index, we probably can't swapp apps.
+ // Tell the user somethign helpful?
+ break;
+
+ case UpdateService.EVENT_COMPLETE_AND_SAME:
+ schedulePollForUpdates();
+ break;
+ }
+ }
+ });
+ }
+ }
+
+ private void schedulePollForUpdates() {
+ Log.d(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);
+ }
+
+ @Override
+ public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
+
+ inflater.inflate(R.menu.swap_search, menu);
+
+ SearchView searchView = new SearchView(getActivity());
+
+ MenuItem searchMenuItem = menu.findItem(R.id.action_search);
+ MenuItemCompat.setActionView(searchMenuItem, searchView);
+ MenuItemCompat.setShowAsAction(searchMenuItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
+
+ searchView.setOnQueryTextListener(this);
+ return true;
+ }
+
+ @Override
+ public int getStep() {
+ return SwapManager.STEP_SUCCESS;
+ }
+
+ @Override
+ public int getPreviousStep() {
+ return SwapManager.STEP_INTRO;
+ }
+
+ @ColorRes
+ public int getToolbarColour() {
+ return getResources().getColor(R.color.swap_bright_blue);
+ }
+
+ @Override
+ public String getToolbarTitle() {
+ return getResources().getString(R.string.swap_success);
+ }
+
+ private void showAppDetails(int position) {
+ Cursor c = (Cursor) adapter.getItem(position);
+ App app = new App(c);
+ // TODO: Show app details screen.
+ }
+
+ @Override
+ public CursorLoader onCreateLoader(int id, Bundle args) {
+ Uri uri = TextUtils.isEmpty(mCurrentFilterString)
+ ? AppProvider.getRepoUri(repo)
+ : AppProvider.getSearchUri(repo, mCurrentFilterString);
+
+ return new CursorLoader(getActivity(), uri, AppProvider.DataColumns.ALL, null, null, AppProvider.DataColumns.NAME);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor cursor) {
+ adapter.swapCursor(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ adapter.swapCursor(null);
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
+ if (mCurrentFilterString == null && newFilter == null) {
+ return true;
+ }
+ if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
+ return true;
+ }
+ mCurrentFilterString = newFilter;
+ getActivity().getSupportLoaderManager().restartLoader(LOADER_SWAPABLE_APPS, null, this);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ // this is not needed since we respond to every change in text
+ return true;
+ }
+
+ private class AppListAdapter extends CursorAdapter {
+
+ @SuppressWarnings("UnusedDeclaration")
+ private static final String TAG = "AppListAdapter";
+
+ @Nullable
+ private LayoutInflater inflater;
+
+ @Nullable
+ private Drawable defaultAppIcon;
+
+ public AppListAdapter(@NonNull Context context, @Nullable Cursor c) {
+ super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
+ }
+
+ @NonNull
+ private LayoutInflater getInflater(Context context) {
+ if (inflater == null) {
+ inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+ return inflater;
+ }
+
+ private Drawable getDefaultAppIcon(Context context) {
+ if (defaultAppIcon == null) {
+ defaultAppIcon = context.getResources().getDrawable(android.R.drawable.sym_def_app_icon);
+ }
+ return defaultAppIcon;
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = getInflater(context).inflate(R.layout.swap_app_list_item, parent, false);
+ bindView(view, context, cursor);
+ return view;
+ }
+
+ @Override
+ public void bindView(final View view, final Context context, final Cursor cursor) {
+
+ TextView nameView = (TextView)view.findViewById(R.id.name);
+ ImageView iconView = (ImageView)view.findViewById(android.R.id.icon);
+ Button button = (Button)view.findViewById(R.id.button);
+ TextView status = (TextView)view.findViewById(R.id.status);
+
+ final App app = new App(cursor);
+
+ nameView.setText(app.name);
+ iconView.setImageDrawable(getDefaultAppIcon(context)); // TODO: Load icon from repo properly using UIL.
+
+ if (app.hasUpdates()) {
+ button.setText(R.string.menu_upgrade);
+ button.setEnabled(true);
+ button.setBackgroundColor(getResources().getColor(R.color.fdroid_blue));
+ button.setVisibility(View.VISIBLE);
+ status.setVisibility(View.GONE);
+ } else if (app.isInstalled()) {
+ status.setText(R.string.inst);
+ status.setTextColor(getResources().getColor(R.color.fdroid_green));
+ status.setVisibility(View.VISIBLE);
+ button.setVisibility(View.GONE);
+ } else if (!app.compatible) {
+ status.setText(R.string.incompatible);
+ status.setTextColor(getResources().getColor(R.color.swap_light_grey_icon));
+ status.setVisibility(View.VISIBLE);
+ button.setVisibility(View.GONE);
+ } else {
+ button.setText(R.string.menu_install);
+ button.setEnabled(true);
+ button.setBackgroundColor(getResources().getColor(R.color.fdroid_green));
+ button.setVisibility(View.VISIBLE);
+ status.setVisibility(View.GONE);
+ }
+
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (app.hasUpdates() || app.compatible) {
+ getState().install(app);
+ }
+ }
+ });
+
+ }
+ }
+
+}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java
index 479a5df31..a4d6015b3 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java
@@ -16,6 +16,7 @@ import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
+import android.widget.Toast;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.ProgressListener;
@@ -24,6 +25,8 @@ import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.localrepo.SwapManager;
import org.fdroid.fdroid.localrepo.peers.Peer;
+import java.util.List;
+
public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity.InnerView {
private final static String TAG = "SwapConnecting";
@@ -77,6 +80,31 @@ public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity
@Override
public void onProgress(Event event) {
((TextView) findViewById(R.id.progress)).setText(event.data.getString(UpdateService.EXTRA_ADDRESS));
+ boolean finished = false;
+ boolean error = false;
+ switch (event.type) {
+ case UpdateService.EVENT_ERROR:
+ finished = true;
+ error = true;
+ break;
+ case UpdateService.EVENT_COMPLETE_WITH_CHANGES:
+ finished = true;
+ break;
+ case UpdateService.EVENT_COMPLETE_AND_SAME:
+ finished = true;
+ break;
+ case UpdateService.EVENT_INFO:
+ break;
+ }
+
+ if (finished) {
+ if (error) {
+ // TODO: Feedback to user about error, suggest fixes.
+ } else {
+ getActivity().showSwapConnected();
+ }
+ }
+
}
});
}
diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
index 8a12ef950..7d20102a8 100644
--- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
+++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java
@@ -237,6 +237,10 @@ public class SwapWorkflowActivity extends AppCompatActivity {
inflateInnerView(R.layout.swap_wifi_qr);
}
+ public void showSwapConnected() {
+ inflateInnerView(R.layout.swap_success);
+ }
+
private boolean attemptToShowNfc() {
// TODO: What if NFC is disabled? Hook up with NfcNotEnabledActivity? Or maybe only if they
// click a relevant button?
@@ -286,9 +290,9 @@ public class SwapWorkflowActivity extends AppCompatActivity {
}
@Override
- protected void onPostExecute(Void v) {
- super.onPostExecute(v);
+ protected void onPreExecute() {
state.enableSwapping();
+ super.onPreExecute();
}
}