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:
parent
8bb0e58e6c
commit
3223e20e33
@ -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
|
||||
|
27
res/layout/repodiscoveryitem.xml
Normal file
27
res/layout/repodiscoveryitem.xml
Normal 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>
|
43
res/layout/repodiscoverylist.xml
Normal file
43
res/layout/repodiscoverylist.xml
Normal 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>
|
@ -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…</string>
|
||||
|
||||
<!--
|
||||
status_download takes four parameters:
|
||||
- Repository (url)
|
||||
|
251
src/org/fdroid/fdroid/net/NsdHelper.java
Normal file
251
src/org/fdroid/fdroid/net/NsdHelper.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user