diff --git a/res/layout/repodiscoveryitem.xml b/res/layout/repodiscoveryitem.xml index c7fc54f88..b621839d7 100644 --- a/res/layout/repodiscoveryitem.xml +++ b/res/layout/repodiscoveryitem.xml @@ -10,7 +10,6 @@ android:maxLines="1" android:paddingLeft="8sp" android:paddingStart="8sp" - android:text="@string/discovered_repo_name" android:textSize="16sp" android:textStyle="bold" /> @@ -23,7 +22,7 @@ android:paddingLeft="8sp" android:paddingStart="8sp" android:maxLines="1" - android:text="@string/repo_address" + android:text="@string/waiting_for_ipaddress" android:textSize="14sp" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 5b21f4912..263bb5da5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -165,6 +165,7 @@ Local FDroid Repos Discovering local FDroid repos… Your local FDroid repo is accessible. + waiting for IP address… Setup Local Repo Touch to setup your local repo. Touch to turn on your local repo. @@ -251,9 +252,6 @@ Your device is not on the same WiFi as the local repo you just added! Try joining this network: %s Requires: %1$s - Discovered Repo Name - Repo Address - App icon Repo icon diff --git a/src/org/fdroid/fdroid/net/MDnsHelper.java b/src/org/fdroid/fdroid/net/MDnsHelper.java new file mode 100644 index 000000000..48e2bc853 --- /dev/null +++ b/src/org/fdroid/fdroid/net/MDnsHelper.java @@ -0,0 +1,249 @@ + +package org.fdroid.fdroid.net; + +import android.app.Activity; +import android.content.Context; +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.*; + +public class MDnsHelper implements ServiceListener { + + public 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; + + public MDnsHelper(Activity activity, final RepoScanListAdapter adapter) { + mActivity = activity; + mAdapter = adapter; + } + + @Override + public void serviceAdded(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.addItem(serviceInfo); + } + }); + } + + @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 serviceResolved(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.addItem(serviceInfo); + } + }); + } + + public void discoverServices() { + new AsyncTask() { + + @Override + protected Void doInBackground(Void... params) { + try { + mJmdns = JmDNS.create(); + } 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() { + 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 Context mContext; + private LayoutInflater mLayoutInflater; + private List 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(); + if (addresses != null && addresses.length > 0) + return true; + else + return false; + } + + @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()); + } + } +} diff --git a/src/org/fdroid/fdroid/net/NsdHelper.java b/src/org/fdroid/fdroid/net/NsdHelper.java deleted file mode 100644 index b56dbe37c..000000000 --- a/src/org/fdroid/fdroid/net/NsdHelper.java +++ /dev/null @@ -1,252 +0,0 @@ -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 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 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() { - @Override - 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()); - } - } -} - - - diff --git a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java index 0fc2b8fa1..7423d7bc2 100644 --- a/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java +++ b/src/org/fdroid/fdroid/views/fragments/RepoListFragment.java @@ -1,19 +1,11 @@ package org.fdroid.fdroid.views.fragments; -import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; -import android.content.ContentValues; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; +import android.content.*; 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; @@ -22,31 +14,18 @@ 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.text.TextUtils; import android.text.format.DateFormat; import android.util.Log; -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; -import android.widget.TextView; -import android.widget.Toast; +import android.view.*; +import android.widget.*; -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.*; import org.fdroid.fdroid.compat.ClipboardCompat; import org.fdroid.fdroid.data.Repo; 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.RepoAdapter; import org.fdroid.fdroid.views.RepoDetailsActivity; @@ -55,6 +34,8 @@ import java.net.URL; import java.util.Date; import java.util.Locale; +import javax.jmdns.ServiceInfo; + public class RepoListFragment extends ListFragment implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { @@ -246,57 +227,56 @@ public class RepoListFragment extends ListFragment }); } - @TargetApi(16) // AKA Android 4.1 AKA Jelly Bean private void scanForRepos() { - final Activity a = getActivity(); + final Activity activity = getActivity(); - final RepoScanListAdapter adapter = new RepoScanListAdapter(a); - final NsdHelper nsdHelper = new NsdHelper(a.getApplicationContext(), adapter); + final RepoScanListAdapter adapter = new RepoScanListAdapter(activity); + final MDnsHelper mDnsHelper = new MDnsHelper(activity, 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(); + .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) { - nsdHelper.stopDiscovery(); + mDnsHelper.stopDiscovery(); } }); repoScanList.setAdapter(adapter); repoScanList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, final View view, - int position, long id) { + @Override + public void onItemClick(AdapterView parent, final View view, + int position, long id) { - final DiscoveredRepo discoveredService = - (DiscoveredRepo) parent.getItemAtPosition(position); + final DiscoveredRepo discoveredService = + (DiscoveredRepo) parent.getItemAtPosition(position); - final NsdServiceInfo serviceInfo = discoveredService.getServiceInfo(); + final ServiceInfo serviceInfo = discoveredService.getServiceInfo(); - String serviceType = serviceInfo.getServiceType(); - String protocol = serviceType.contains("fdroidrepos") ? "https://" : "http://"; + String serviceType = serviceInfo.getType(); + // TODO get repo type from TXT record + String protocol = serviceType.contains("fdroidrepos") ? "https:/" : "http:/"; - String serviceAddress = protocol + serviceInfo.getHost().getHostAddress() - + ":" + serviceInfo.getPort() + "/fdroid/repo"; - showAddRepo(serviceAddress, ""); - } + String serviceAddress = protocol + serviceInfo.getInetAddresses()[0] + + ":" + serviceInfo.getPort() + "/fdroid/repo"; + // TODO get path from TXT record + showAddRepo(serviceAddress, ""); + } }); alrt.show(); - - Log.d("FDroid", "Starting network service discovery"); - nsdHelper.discoverServices(); + mDnsHelper.discoverServices(); } public void importRepo(String uri, String fingerprint) { @@ -416,7 +396,8 @@ public class RepoListFragment extends ListFragment ContentValues values = new ContentValues(2); values.put(RepoProvider.DataColumns.ADDRESS, address); if (fingerprint != null && fingerprint.length() > 0) { - values.put(RepoProvider.DataColumns.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); + values.put(RepoProvider.DataColumns.FINGERPRINT, + fingerprint.toUpperCase(Locale.ENGLISH)); } RepoProvider.Helper.insert(getActivity(), values); finishedAddingRepo();