WIP: Refactoring Bonjour from manage repos to swap.

Implementing the bare bones of a generic "peer finder" framework. This
may or may not eventuate to something which can live in its own library
and be used by other projects. Might go hand in hand with Carries idea
of having a common UI to be shared among projects.
This commit is contained in:
Peter Serwylo 2015-06-13 00:13:05 +10:00
parent 593aaf3894
commit a30ec646b2
14 changed files with 313 additions and 333 deletions

View File

@ -12,9 +12,5 @@
android:icon="@drawable/ic_add_white" android:icon="@drawable/ic_add_white"
android:title="@string/menu_add_repo" android:title="@string/menu_add_repo"
app:showAsAction="always|withText"/> app:showAsAction="always|withText"/>
<item
android:id="@+id/action_find_local_repos"
android:title="@string/menu_scan_repo"
app:showAsAction="ifRoom|withText"/>
</menu> </menu>

View File

@ -113,7 +113,6 @@
<string name="menu_search">Search</string> <string name="menu_search">Search</string>
<string name="menu_add_repo">New Repository</string> <string name="menu_add_repo">New Repository</string>
<string name="menu_rem_repo">Remove Repository</string> <string name="menu_rem_repo">Remove Repository</string>
<string name="menu_scan_repo">Find Local Repos</string>
<string name="menu_launch">Run</string> <string name="menu_launch">Run</string>
<string name="menu_share">Share</string> <string name="menu_share">Share</string>

View File

@ -12,8 +12,13 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.localrepo.peers.PeerFinder;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -43,9 +48,13 @@ public class SwapManager {
@NonNull @NonNull
private Set<String> appsToSwap; private Set<String> appsToSwap;
@NonNull
private Collection<Peer> peers;
private SwapManager(@NonNull Context context, @NonNull Set<String> appsToSwap) { private SwapManager(@NonNull Context context, @NonNull Set<String> appsToSwap) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.appsToSwap = appsToSwap; this.appsToSwap = appsToSwap;
this.peers = new ArrayList<>();
setupService(); setupService();
} }
@ -60,6 +69,32 @@ public class SwapManager {
return context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_APPEND); return context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_APPEND);
} }
// ==========================================================
// Search for peers to swap
// ==========================================================
public void scanForPeers() {
if (service != null) {
Log.d(TAG, "Scanning for nearby devices to swap with...");
service.scanForPeers();
} else {
Log.e(TAG, "Couldn't scan for peers, because service was not running.");
}
}
public void cancelScanningForPeers() {
if (service != null) {
service.cancelScanningForPeers();
} else {
Log.e(TAG, "Couldn't cancel scanning for peers, because service was not running.");
}
}
public void onPeerFound(Peer peer) {
peers.add(peer);
}
// ========================================================== // ==========================================================
// Manage the current step // Manage the current step
// ("Step" refers to the current view being shown in the UI) // ("Step" refers to the current view being shown in the UI)

View File

@ -16,6 +16,12 @@ import android.util.Log;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.peers.BluetoothFinder;
import org.fdroid.fdroid.localrepo.peers.BluetoothPeer;
import org.fdroid.fdroid.localrepo.peers.BonjourFinder;
import org.fdroid.fdroid.localrepo.peers.BonjourPeer;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.localrepo.peers.PeerFinder;
import org.fdroid.fdroid.localrepo.type.BonjourType; import org.fdroid.fdroid.localrepo.type.BonjourType;
import org.fdroid.fdroid.localrepo.type.NfcType; import org.fdroid.fdroid.localrepo.type.NfcType;
import org.fdroid.fdroid.localrepo.type.WebServerType; import org.fdroid.fdroid.localrepo.type.WebServerType;
@ -42,6 +48,10 @@ public class SwapService extends Service {
private final BonjourType bonjourType; private final BonjourType bonjourType;
private final WebServerType webServerType; private final WebServerType 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 // 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 // to a specific _Activity_, and will only be active while that activity is shown. This service
// knows nothing about activities. // knows nothing about activities.
@ -65,6 +75,22 @@ public class SwapService extends Service {
nfcType = new NfcType(this); nfcType = new NfcType(this);
bonjourType = new BonjourType(this); bonjourType = new BonjourType(this);
webServerType = new WebServerType(this); webServerType = new WebServerType(this);
bonjourFinder = new BonjourFinder(this);
bluetoothFinder = new BluetoothFinder(this);
bonjourFinder.setListener(new PeerFinder.Listener<BonjourPeer>() {
@Override
public void onPeerFound(BonjourPeer peer) {
SwapManager.load(SwapService.this).onPeerFound(peer);
}
});
bluetoothFinder.setListener(new PeerFinder.Listener<BluetoothPeer>() {
@Override
public void onPeerFound(BluetoothPeer peer) {
SwapManager.load(SwapService.this).onPeerFound(peer);
}
});
} }
public void onCreate() { public void onCreate() {
@ -104,6 +130,20 @@ public class SwapService extends Service {
.build(); .build();
} }
public void scanForPeers() {
bonjourFinder.scan();
bluetoothFinder.scan();
}
public void cancelScanningForPeers() {
bonjourFinder.cancel();
bluetoothFinder.cancel();
}
public void onPeerFound(Peer peer) {
SwapManager.load(this).onPeerFound(peer);
}
private boolean enabled = false; private boolean enabled = false;
/** /**

View File

@ -0,0 +1,21 @@
package org.fdroid.fdroid.localrepo.peers;
import android.content.Context;
// TODO: Still to be implemented
public class BluetoothFinder extends PeerFinder<BluetoothPeer> {
private static final String TAG = "BluetoothFinder";
public BluetoothFinder(Context context) {
}
@Override
public void scan() {
}
@Override
public void cancel() {
}
}

View File

@ -0,0 +1,24 @@
package org.fdroid.fdroid.localrepo.peers;
import android.bluetooth.BluetoothDevice;
// TODO: Still to be implemented.
public class BluetoothPeer implements Peer {
private BluetoothDevice device;
public BluetoothPeer(BluetoothDevice device) {
this.device = device;
}
@Override
public String getName() {
return "Bluetooth: " + device.getName();
}
@Override
public int getIcon() {
return android.R.drawable.stat_sys_data_bluetooth;
}
}

View File

@ -0,0 +1,114 @@
package org.fdroid.fdroid.localrepo.peers;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
public class BonjourFinder extends PeerFinder<BonjourPeer> implements ServiceListener {
private static final String TAG = "BonjourFinder";
public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
private final Context context;
private JmDNS mJmdns;
private WifiManager wifiManager;
private WifiManager.MulticastLock mMulticastLock;
public BonjourFinder(Context context) {
this.context = context;
}
@Override
public void scan() {
if (wifiManager == null) {
wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(context.getPackageName());
mMulticastLock.setReferenceCounted(false);
}
mMulticastLock.acquire();
new AsyncTask<Void, Void, Void>() {
@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.");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void result) {
Log.d(TAG, "Cleaning up mDNS service listeners.");
if (mJmdns != null) {
mJmdns.addServiceListener(HTTP_SERVICE_TYPE, BonjourFinder.this);
mJmdns.addServiceListener(HTTPS_SERVICE_TYPE, BonjourFinder.this);
}
}
}.execute();
}
@Override
public void serviceRemoved(ServiceEvent event) {
}
@Override
public void serviceAdded(final ServiceEvent event) {
addFDroidService(event);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mJmdns.requestServiceInfo(event.getType(), event.getName(), true);
return null;
}
}.execute();
}
@Override
public void serviceResolved(ServiceEvent event) {
addFDroidService(event);
}
private void addFDroidService(ServiceEvent event) {
final ServiceInfo serviceInfo = event.getInfo();
if (serviceInfo.getPropertyString("type").startsWith("fdroidrepo")) {
foundPeer(new BonjourPeer(serviceInfo));
}
}
@Override
public void cancel() {
mMulticastLock.release();
if (mJmdns == null)
return;
mJmdns.removeServiceListener(HTTP_SERVICE_TYPE, this);
mJmdns.removeServiceListener(HTTPS_SERVICE_TYPE, this);
mJmdns = null;
}
}

View File

@ -0,0 +1,25 @@
package org.fdroid.fdroid.localrepo.peers;
import org.fdroid.fdroid.R;
import javax.jmdns.ServiceInfo;
public class BonjourPeer implements Peer {
private ServiceInfo serviceInfo;
public BonjourPeer(ServiceInfo serviceInfo) {
this.serviceInfo = serviceInfo;
}
@Override
public String getName() {
return "Bonjour: " + serviceInfo.getName();
}
@Override
public int getIcon() {
return R.drawable.wifi;
}
}

View File

@ -0,0 +1,11 @@
package org.fdroid.fdroid.localrepo.peers;
import android.support.annotation.DrawableRes;
public interface Peer {
String getName();
@DrawableRes int getIcon();
}

View File

@ -0,0 +1,41 @@
package org.fdroid.fdroid.localrepo.peers;
import android.content.Intent;
import android.util.Log;
/**
* Searches for other devices in the vicinity, using specific technologies.
* Once found, alerts a listener through the
* {@link org.fdroid.fdroid.localrepo.peers.PeerFinder.Listener#onPeerFound(Object)}
* method. Note that this could have instead been done with {@link android.content.Context#sendBroadcast(Intent)}
* and {@link android.content.BroadcastReceiver}, but that would require making the {@link Peer}s
* {@link android.os.Parcelable}, which is difficult. The main reason it is difficult is because
* they encapsulate information about network connectivity, such as {@link android.bluetooth.BluetoothDevice}
* and {@link javax.jmdns.ServiceInfo}, which may be difficult to serialize and reconstruct again.
*/
public abstract class PeerFinder<T extends Peer> {
private static final String TAG = "PeerFinder";
private Listener<T> listener;
public abstract void scan();
public abstract void cancel();
protected void foundPeer(T peer) {
Log.i(TAG, "Found peer " + peer.getName());
if (listener != null) {
listener.onPeerFound(peer);
}
}
public void setListener(Listener<T> listener) {
this.listener = listener;
}
public interface Listener<T> {
void onPeerFound(T peer);
// TODO: What about peers removed, e.g. as with jmdns ServiceListener#serviceRemoved()
}
}

View File

@ -15,7 +15,7 @@ import javax.jmdns.ServiceInfo;
public class BonjourType implements SwapType { public class BonjourType implements SwapType {
private static final String TAG = "BonjourType"; private static final String TAG = "BonjourBroadcastType";
private JmDNS jmdns; private JmDNS jmdns;
private ServiceInfo pairService; private ServiceInfo pairService;

View File

@ -1,270 +0,0 @@
package org.fdroid.fdroid.net;
import android.app.Activity;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.fdroid.fdroid.R;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
public class MDnsHelper implements ServiceListener {
private static final String TAG = "MDnsHelper";
public static final String HTTP_SERVICE_TYPE = "_http._tcp.local.";
public static final String HTTPS_SERVICE_TYPE = "_https._tcp.local.";
final Activity mActivity;
final RepoScanListAdapter mAdapter;
private JmDNS mJmdns;
private final WifiManager wifiManager;
private final MulticastLock mMulticastLock;
public MDnsHelper(Activity activity, final RepoScanListAdapter adapter) {
mActivity = activity;
mAdapter = adapter;
wifiManager = (WifiManager) activity.getSystemService(Context.WIFI_SERVICE);
mMulticastLock = wifiManager.createMulticastLock(activity.getPackageName());
mMulticastLock.setReferenceCounted(false);
}
@Override
public void serviceRemoved(ServiceEvent event) {
// a ListView Adapter can only be updated on the UI thread
final ServiceInfo serviceInfo = event.getInfo();
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.removeItem(serviceInfo);
}
});
}
@Override
public void serviceAdded(final ServiceEvent event) {
addFDroidService(event);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mJmdns.requestServiceInfo(event.getType(), event.getName(), true);
return null;
}
}.execute();
}
@Override
public void serviceResolved(ServiceEvent event) {
addFDroidService(event);
}
private void addFDroidService(ServiceEvent event) {
// a ListView Adapter can only be updated on the UI thread
final ServiceInfo serviceInfo = event.getInfo();
String type = serviceInfo.getPropertyString("type");
if (type.startsWith("fdroidrepo"))
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.addItem(serviceInfo);
}
});
}
public void discoverServices() {
mMulticastLock.acquire();
new AsyncTask<Void, Void, Void>() {
@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)
};
mJmdns = JmDNS.create(InetAddress.getByAddress(byteIp));
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (mJmdns != null) {
mJmdns.addServiceListener(HTTP_SERVICE_TYPE, MDnsHelper.this);
mJmdns.addServiceListener(HTTPS_SERVICE_TYPE, MDnsHelper.this);
}
}
}.execute();
}
public void stopDiscovery() {
mMulticastLock.release();
if (mJmdns == null)
return;
mJmdns.removeServiceListener(HTTP_SERVICE_TYPE, MDnsHelper.this);
mJmdns.removeServiceListener(HTTPS_SERVICE_TYPE, MDnsHelper.this);
mJmdns = null;
}
public static class RepoScanListAdapter extends BaseAdapter {
private final Context mContext;
private final LayoutInflater mLayoutInflater;
private final List<DiscoveredRepo> mEntries = new ArrayList<>();
public RepoScanListAdapter(Context context) {
mContext = context;
mLayoutInflater = (LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return mEntries.size();
}
@Override
public Object getItem(int position) {
return mEntries.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean isEnabled(int position) {
DiscoveredRepo service = mEntries.get(position);
ServiceInfo serviceInfo = service.getServiceInfo();
InetAddress[] addresses = serviceInfo.getInetAddresses();
return (addresses != null && addresses.length > 0);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RelativeLayout itemView;
if (convertView == null) {
itemView = (RelativeLayout) mLayoutInflater.inflate(
R.layout.repodiscoveryitem, parent, false);
} else {
itemView = (RelativeLayout) convertView;
}
TextView nameLabel = (TextView) itemView.findViewById(R.id.reposcanitemname);
TextView addressLabel = (TextView) itemView.findViewById(R.id.reposcanitemaddress);
final DiscoveredRepo service = mEntries.get(position);
final ServiceInfo serviceInfo = service.getServiceInfo();
nameLabel.setText(serviceInfo.getName());
InetAddress[] addresses = serviceInfo.getInetAddresses();
if (addresses != null && addresses.length > 0) {
String addressTxt = "Hosted @ " + addresses[0] + ":" + serviceInfo.getPort();
addressLabel.setText(addressTxt);
}
return itemView;
}
public void addItem(ServiceInfo item) {
if (item == null || item.getName() == null)
return;
// Construct a DiscoveredRepo wrapper for the service being
// added in order to use a name based equals().
DiscoveredRepo newDRepo = new DiscoveredRepo(item);
// if an unresolved entry with the same name exists, remove it
for (DiscoveredRepo dr : mEntries)
if (dr.equals(newDRepo)) {
InetAddress[] addresses = dr.mServiceInfo.getInetAddresses();
if (addresses == null || addresses.length == 0)
mEntries.remove(dr);
}
mEntries.add(newDRepo);
notifyUpdate();
}
public void removeItem(ServiceInfo item) {
if (item == null || item.getName() == null)
return;
// Construct a DiscoveredRepo wrapper for the service being
// removed in order to use a name based equals().
DiscoveredRepo lostServiceBean = new DiscoveredRepo(item);
if (mEntries.contains(lostServiceBean)) {
mEntries.remove(lostServiceBean);
notifyUpdate();
}
}
private void notifyUpdate() {
// Need to call notifyDataSetChanged from the UI thread
// in order for it to update the ListView without error
Handler refresh = new Handler(Looper.getMainLooper());
refresh.post(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
}
public static class DiscoveredRepo {
private final ServiceInfo mServiceInfo;
public DiscoveredRepo(ServiceInfo serviceInfo) {
if (serviceInfo == null || serviceInfo.getName() == null)
throw new IllegalArgumentException(
"Parameters \"serviceInfo\" and \"name\" must not be null.");
mServiceInfo = serviceInfo;
}
public ServiceInfo getServiceInfo() {
return mServiceInfo;
}
public String getName() {
return mServiceInfo.getName();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DiscoveredRepo))
return false;
// Treat two services the same based on name. Eventually
// there should be a persistent mapping between fingerprint
// of the repo key and the discovered service such that we
// could maintain trust across hostnames/ips/networks
DiscoveredRepo otherRepo = (DiscoveredRepo) other;
return getName().equals(otherRepo.getName());
}
}
}

View File

@ -71,9 +71,6 @@ import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.MDnsHelper;
import org.fdroid.fdroid.net.MDnsHelper.DiscoveredRepo;
import org.fdroid.fdroid.net.MDnsHelper.RepoScanListAdapter;
import org.fdroid.fdroid.views.fragments.RepoDetailsFragment; import org.fdroid.fdroid.views.fragments.RepoDetailsFragment;
import java.io.IOException; import java.io.IOException;
@ -84,8 +81,6 @@ import java.net.URL;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import javax.jmdns.ServiceInfo;
public class ManageReposActivity extends ActionBarActivity { public class ManageReposActivity extends ActionBarActivity {
/** /**
@ -212,9 +207,6 @@ public class ManageReposActivity extends ActionBarActivity {
case R.id.action_update_repo: case R.id.action_update_repo:
updateRepos(); updateRepos();
return true; return true;
case R.id.action_find_local_repos:
scanForRepos();
return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -239,55 +231,6 @@ public class ManageReposActivity extends ActionBarActivity {
}); });
} }
private void scanForRepos() {
final RepoScanListAdapter adapter = new RepoScanListAdapter(this);
final MDnsHelper mDnsHelper = new MDnsHelper(this, adapter);
final View view = getLayoutInflater().inflate(R.layout.repodiscoverylist, null);
final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist);
final AlertDialog alrt = new AlertDialog.Builder(this).setView(view)
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mDnsHelper.stopDiscovery();
dialog.dismiss();
}
}).create();
alrt.setTitle(R.string.local_repos_title);
alrt.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
mDnsHelper.stopDiscovery();
}
});
repoScanList.setAdapter(adapter);
repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, final View view,
int position, long id) {
final DiscoveredRepo discoveredService =
(DiscoveredRepo) parent.getItemAtPosition(position);
final ServiceInfo serviceInfo = discoveredService.getServiceInfo();
String type = serviceInfo.getPropertyString("type");
String protocol = type.contains("fdroidrepos") ? "https:/" : "http:/";
String path = serviceInfo.getPropertyString("path");
if (TextUtils.isEmpty(path))
path = "/fdroid/repo";
String serviceUrl = protocol + serviceInfo.getInetAddresses()[0]
+ ":" + serviceInfo.getPort() + path;
showAddRepo(serviceUrl, serviceInfo.getPropertyString("fingerprint"));
}
});
alrt.show();
mDnsHelper.discoverServices();
}
private void showAddRepo() { private void showAddRepo() {
/* /*
* If there is text in the clipboard, and it looks like a URL, use that. * If there is text in the clipboard, and it looks like a URL, use that.

View File

@ -165,6 +165,7 @@ public class SwapWorkflowActivity extends ActionBarActivity {
} }
private void showIntro() { private void showIntro() {
SwapManager.load(this).scanForPeers();
inflateInnerView(R.layout.swap_blank); inflateInnerView(R.layout.swap_blank);
} }