Add support for Network Service Discovery of FDroid repos.

If the device supports API level 16 (Android 4.1) then add a menu item
on the repository management screen to "Find Local Repos". Activating
this menu item will initiate NSD service discovery with the NsdHelper
class looking for 'fdroidrepo' and 'fdroidrepos' service types on the
local network. When one is found, the service is resolved and the name
& IP are populated into a list of discovered repositories. Clicking an
NSD discovered repo will prompt the user to add the repo.
This commit is contained in:
Daniel McCarney 2014-02-19 14:52:01 -05:00
parent 8bb0e58e6c
commit 3223e20e33
6 changed files with 403 additions and 1 deletions

View File

@ -1,5 +1,8 @@
### Upcoming release
* Support for Network Service Discovery of local FDroid repos on Android 4.1+
from the repository management screen.
* Always remember the selected category in the list of apps
* Send FDroid via Bluetooth to any device that supports receiving APKs via

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/reposcanitemname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingLeft="8sp"
android:text="Discovered Repo Name"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/reposcanitemaddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/reposcanitemname"
android:layout_marginTop="2dp"
android:paddingLeft="8sp"
android:maxLines="1"
android:text="Repo Address"
android:textSize="14sp" />
</RelativeLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="@+id/reposcanprogresslayout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_centerHorizontal="true"
android:paddingTop="8sp" >
<ProgressBar
android:id="@+id/reposcaningspinner"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginRight="5dp"
android:indeterminate="true" />
<TextView
android:id="@+id/reposcaninglabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="8sp"
android:text="@string/local_repos_scanning"
android:textSize="15sp" />
</LinearLayout>
<ListView
android:id="@+id/reposcanlist"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/reposcanprogresslayout"
android:padding="8sp"
/>
</RelativeLayout>

View File

@ -100,6 +100,7 @@
<string name="menu_search">Search</string>
<string name="menu_add_repo">New 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_share">Share</string>
@ -148,6 +149,9 @@
<string name="category_whatsnew">What\'s New</string>
<string name="category_recentlyupdated">Recently Updated</string>
<string name="local_repos_title">Local FDroid Repos</string>
<string name="local_repos_scanning">Discovering local FDroid repos&#8230;</string>
<!--
status_download takes four parameters:
- Repository (url)

View File

@ -0,0 +1,251 @@
package org.fdroid.fdroid.net;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.nsd.NsdServiceInfo;
import android.net.nsd.NsdManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
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.util.ArrayList;
import java.util.List;
@TargetApi(16) // AKA Android 4.1 AKA Jelly Bean
public class NsdHelper {
public static final String TAG = "NsdHelper";
public static final String HTTP_SERVICE_TYPE = "_fdroidrepo._tcp.";
public static final String HTTPS_SERVICE_TYPE = "_fdroidrepos._tcp.";
final Context mContext;
final NsdManager mNsdManager;
final RepoScanListAdapter mAdapter;
NsdManager.ResolveListener mResolveListener;
NsdManager.DiscoveryListener mDiscoveryListener;
public NsdHelper(Context context, final RepoScanListAdapter adapter) {
mContext = context;
mAdapter = adapter;
mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
initializeResolveListener();
initializeDiscoveryListener();
}
public void initializeDiscoveryListener() {
mDiscoveryListener = new NsdManager.DiscoveryListener() {
@Override
public void onDiscoveryStarted(String regType) {
Log.i(TAG, "Service discovery started");
}
@Override
public void onServiceFound(NsdServiceInfo service)
{
Log.d(TAG, "Discovered service: "+ service.getServiceName() +
" Type: "+ service.getServiceType());
if (service.getServiceType().equals(HTTP_SERVICE_TYPE) ||
service.getServiceType().equals(HTTPS_SERVICE_TYPE))
{
Log.d(TAG, "Resolving FDroid service");
mNsdManager.resolveService(service, mResolveListener);
}
}
@Override
public void onServiceLost(NsdServiceInfo service) {
Log.e(TAG, "service lost" + service);
mAdapter.removeItem(service);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "Discovery stopped: " + serviceType);
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
};
}
public void initializeResolveListener() {
mResolveListener = new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.e(TAG, "Resolve failed: Error code: " + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.d(TAG, "Resolve Succeeded. " + serviceInfo);
mAdapter.addItem(serviceInfo);
}
};
}
public void discoverServices() {
mNsdManager.discoverServices(
HTTP_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
mNsdManager.discoverServices(
HTTPS_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
}
public void stopDiscovery() {
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
}
public static class RepoScanListAdapter extends BaseAdapter {
private Context mContext;
private LayoutInflater mLayoutInflater;
private List<DiscoveredRepo> mEntries = new ArrayList<DiscoveredRepo>();
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 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 NsdServiceInfo serviceInfo = service.getServiceInfo();
String addressTxt = "Hosted @ "+
serviceInfo.getHost().getHostAddress() + ":"+ serviceInfo.getPort();
nameLabel.setText(serviceInfo.getServiceName());
addressLabel.setText(addressTxt);
return itemView;
}
public void addItem(NsdServiceInfo item)
{
if(item == null || item.getServiceName() == null)
return;
//Construct a DiscoveredRepo wrapper for the service being
//added in order to use a name based equals().
DiscoveredRepo repoBean = new DiscoveredRepo(item);
mEntries.add(repoBean);
notifyUpdate();
}
public void removeItem(NsdServiceInfo item)
{
if(item == null || item.getServiceName() == 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() {
public void run()
{
notifyDataSetChanged();
}
});
}
}
public static class DiscoveredRepo {
private final NsdServiceInfo mServiceInfo;
public DiscoveredRepo(NsdServiceInfo serviceInfo)
{
if(serviceInfo == null || serviceInfo.getServiceName() == null)
throw new IllegalArgumentException(
"Parameters \"serviceInfo\" and \"name\" must not be null.");
mServiceInfo = serviceInfo;
}
public NsdServiceInfo getServiceInfo()
{
return mServiceInfo;
}
public String getName()
{
return mServiceInfo.getServiceName();
}
@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

@ -1,6 +1,7 @@
package org.fdroid.fdroid.views.fragments;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
@ -10,8 +11,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.net.nsd.NsdServiceInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.ListFragment;
@ -26,6 +29,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
@ -33,9 +37,12 @@ import android.widget.TextView;
import android.widget.Toast;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.net.NsdHelper;
import org.fdroid.fdroid.net.NsdHelper.DiscoveredRepo;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.net.NsdHelper.RepoScanListAdapter;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.compat.ClipboardCompat;
import org.fdroid.fdroid.data.Repo;
@ -54,6 +61,7 @@ public class RepoListFragment extends ListFragment
private static final String DEFAULT_NEW_REPO_TEXT = "https://";
private final int ADD_REPO = 1;
private final int UPDATE_REPOS = 2;
private final int SCAN_FOR_REPOS = 3;
private WifiManager wifiManager;
@ -277,6 +285,12 @@ public class RepoListFragment extends ListFragment
MenuItemCompat.setShowAsAction(addItem,
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
if (Build.VERSION.SDK_INT >= 16)
{
menu.add(Menu.NONE, SCAN_FOR_REPOS, 1, R.string.menu_scan_repo).setIcon(
android.R.drawable.ic_menu_search);
}
}
public static final int SHOW_REPO_DETAILS = 1;
@ -300,6 +314,59 @@ public class RepoListFragment extends ListFragment
});
}
@TargetApi(16) // AKA Android 4.1 AKA Jelly Bean
private void scanForRepos() {
final Activity a = getActivity();
final RepoScanListAdapter adapter = new RepoScanListAdapter(a);
final NsdHelper nsdHelper = new NsdHelper(a.getApplicationContext(), adapter);
final View view = getLayoutInflater(null).inflate(R.layout.repodiscoverylist, null);
final ListView repoScanList = (ListView) view.findViewById(R.id.reposcanlist);
final AlertDialog alrt = new AlertDialog.Builder(getActivity()).setView(view)
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
nsdHelper.stopDiscovery();
dialog.dismiss();
}
}).create();
alrt.setTitle(R.string.local_repos_title);
alrt.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
nsdHelper.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 NsdServiceInfo serviceInfo = discoveredService.getServiceInfo();
String serviceType = serviceInfo.getServiceType();
String protocol = serviceType.contains("fdroidrepos") ? "https://" : "http://";
String serviceAddress = protocol + serviceInfo.getHost().getHostAddress()
+ ":" + serviceInfo.getPort() + "/fdroid/repo";
showAddRepo(serviceAddress, "");
}
});
alrt.show();
Log.d("FDroid", "Starting network service discovery");
nsdHelper.discoverServices();
}
private void showAddRepo() {
showAddRepo(getNewRepoUri(), null);
}
@ -405,9 +472,13 @@ public class RepoListFragment extends ListFragment
* Adds a new repo to the database.
*/
private void createNewRepo(String address, String fingerprint) {
if(fingerprint != null) // Value of null used for no fingerprint by caller
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
ContentValues values = new ContentValues(2);
values.put(RepoProvider.DataColumns.ADDRESS, address);
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint);
RepoProvider.Helper.insert(getActivity(), values);
finishedAddingRepo();
}
@ -445,6 +516,9 @@ public class RepoListFragment extends ListFragment
} else if (item.getItemId() == UPDATE_REPOS) {
updateRepos();
return true;
} else if (item.getItemId() == SCAN_FOR_REPOS) {
scanForRepos();
return true;
}
return super.onOptionsItemSelected(item);