From 615e559ce1fda174f26880cfd528a53e50f9d8d1 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 13 Apr 2018 00:20:52 +0200 Subject: [PATCH 01/11] only prompt to swap back to proper swap URLs Before, it was possible to annoy the user by sending HTTP POST with any repo URL in it. --- .../fdroid/net/BluetoothDownloader.java | 10 ++++++ .../org/fdroid/fdroid/net/HttpDownloader.java | 15 +++++--- .../views/swap/SwapWorkflowActivity.java | 19 +++++++--- app/src/main/res/values/strings.xml | 1 + .../fdroid/fdroid/net/HttpDownloaderTest.java | 36 +++++++++++++++++++ 5 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java diff --git a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java index 85d4f4d84..cb4f54abf 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -14,7 +14,12 @@ import org.fdroid.fdroid.net.bluetooth.httpish.Response; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.regex.Pattern; +/** + * Download from a Bluetooth swap repo. Example URI: + * {@code bluetooth://84-CF-BF-8B-3E-34/fdroid/repo} + */ public class BluetoothDownloader extends Downloader { private static final String TAG = "BluetoothDownloader"; @@ -23,6 +28,11 @@ public class BluetoothDownloader extends Downloader { private FileDetails fileDetails; private final String sourcePath; + public static boolean isBluetoothUri(Uri uri) { + return SCHEME.equals(uri.getScheme()) + && Pattern.matches("([0-9A-F]{2}-)+[0-9A-F]{2}", uri.getHost()); + } + public BluetoothDownloader(Uri uri, File destFile) throws IOException { super(uri, destFile); String macAddress = uri.getHost().replace("-", ":"); diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 880d48fa5..619f48c00 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -122,16 +122,23 @@ public class HttpDownloader extends Downloader { cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG); } - private boolean isSwapUrl() { - String host = sourceUrl.getHost(); - return sourceUrl.getPort() > 1023 // only root can use <= 1023, so never a swap repo + public static boolean isSwapUrl(Uri uri) { + return isSwapUrl(uri.getHost(), uri.getPort()); + } + + public static boolean isSwapUrl(URL url) { + return isSwapUrl(url.getHost(), url.getPort()); + } + + public static boolean isSwapUrl(String host, int port) { + return port > 1023 // only root can use <= 1023, so never a swap repo && host.matches("[0-9.]+") // host must be an IP address && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are } private HttpURLConnection getConnection() throws SocketTimeoutException, IOException { HttpURLConnection connection; - if (isSwapUrl()) { + if (isSwapUrl(sourceUrl)) { // swap never works with a proxy, its unrouted IP on the same subnet connection = (HttpURLConnection) sourceUrl.openConnection(); } else { diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 4bbfbd4f1..f889ca834 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -29,10 +29,9 @@ import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; - +import cc.mvdan.accesspoint.WifiApControl; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; @@ -47,6 +46,8 @@ import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.localrepo.LocalRepoManager; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; +import org.fdroid.fdroid.net.BluetoothDownloader; +import org.fdroid.fdroid.net.HttpDownloader; import java.util.Arrays; import java.util.Date; @@ -57,8 +58,6 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; -import cc.mvdan.accesspoint.WifiApControl; - /** * This activity will do its best to show the most relevant screen about swapping to the user. * The problem comes when there are two competing goals - 1) Show the user a list of apps from another @@ -209,8 +208,20 @@ public class SwapWorkflowActivity extends AppCompatActivity { showRelevantView(); } + /** + * Check whether incoming {@link Intent} is a swap repo, and ensure that + * it is a valid swap URL. The hostname can only be either an IP or + * Bluetooth address. + */ private void checkIncomingIntent() { Intent intent = getIntent(); + Uri uri = intent.getData(); + if (uri != null && !HttpDownloader.isSwapUrl(uri) && !BluetoothDownloader.isBluetoothUri(uri)) { + String msg = getString(R.string.swap_toast_invalid_url, uri); + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); + return; + } + if (intent.getBooleanExtra(EXTRA_CONFIRM, false) && !intent.getBooleanExtra(EXTRA_SWAP_INTENT_HANDLED, false)) { // Storing config in this variable will ensure that when showRelevantView() is next // run, it will show the connect swap view (if the service is available). diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a38fd786a..24456d40f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -460,6 +460,7 @@ This often occurs with apps installed via Google Play or other sources, if they Error occurred while connecting to device, can\'t swap with it! Swapping not enabled Before swapping, your device must be made visible. + Invalid URL for swapping: %1$s needs access to Do you want to install an update diff --git a/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java b/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java new file mode 100644 index 000000000..d5aee05c8 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/net/HttpDownloaderTest.java @@ -0,0 +1,36 @@ +package org.fdroid.fdroid.net; + +import android.net.Uri; +import org.apache.commons.net.util.SubnetUtils; +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.FDroidApp; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +@Config(constants = BuildConfig.class, sdk = 24) +@RunWith(RobolectricTestRunner.class) +@SuppressWarnings("LineLength") +public class HttpDownloaderTest { + + @Test + public void testIsSwapUri() throws MalformedURLException { + FDroidApp.subnetInfo = new SubnetUtils("192.168.0.112/24").getInfo(); + String urlString = "http://192.168.0.112:8888/fdroid/repo?fingerprint=113F56CBFA967BA825DD13685A06E35730E0061C6BB046DF88A"; + assertTrue(HttpDownloader.isSwapUrl("192.168.0.112", 8888)); // NOPMD + assertTrue(HttpDownloader.isSwapUrl(Uri.parse(urlString))); + assertTrue(HttpDownloader.isSwapUrl(new URL(urlString))); + + assertFalse(HttpDownloader.isSwapUrl("192.168.1.112", 8888)); // NOPMD + assertFalse(HttpDownloader.isSwapUrl("192.168.0.112", 80)); // NOPMD + assertFalse(HttpDownloader.isSwapUrl(Uri.parse("https://malware.com:8888"))); + assertFalse(HttpDownloader.isSwapUrl(new URL("https://www.google.com"))); + } +} From 9cb53e93d0470b356d3eee58e272713d90223555 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 10 Apr 2018 22:48:53 +0200 Subject: [PATCH 02/11] allow Bluetooth swap to work when there is no available internet #1393 --- app/src/main/java/org/fdroid/fdroid/UpdateService.java | 7 +++++-- .../java/org/fdroid/fdroid/net/BluetoothDownloader.java | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index ab0dc40dc..20360a5f8 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -47,6 +47,7 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.installer.InstallManagerService; +import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.ConnectivityMonitorService; import org.fdroid.fdroid.views.main.MainActivity; @@ -332,7 +333,7 @@ public class UpdateService extends IntentService { boolean forcedUpdate = false; String address = null; if (intent != null) { - address = intent.getStringExtra(EXTRA_ADDRESS); + address = intent.getStringExtra(EXTRA_ADDRESS); // TODO switch to Intent.setData() manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false); forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false); } @@ -340,7 +341,9 @@ public class UpdateService extends IntentService { try { // See if it's time to actually do anything yet... int netState = ConnectivityMonitorService.getNetworkState(this); - if (netState == ConnectivityMonitorService.FLAG_NET_UNAVAILABLE) { + if (address != null && address.startsWith(BluetoothDownloader.SCHEME)) { + Utils.debugLog(TAG, "skipping internet check, this is bluetooth"); + } else if (netState == ConnectivityMonitorService.FLAG_NET_UNAVAILABLE) { Utils.debugLog(TAG, "No internet, cannot update"); if (manualUpdate) { sendNoInternetToast(); diff --git a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java index cb4f54abf..225be7434 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -24,6 +24,8 @@ public class BluetoothDownloader extends Downloader { private static final String TAG = "BluetoothDownloader"; + public static final String SCHEME = "bluetooth"; + private final BluetoothConnection connection; private FileDetails fileDetails; private final String sourcePath; From 6b4b85d3960fcd2e49d87cea3b84f322a93aa403 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 12 Apr 2018 23:16:33 +0200 Subject: [PATCH 03/11] entirely purge AndroidHttpClient from the code base. closes #586 --- app/build.gradle | 1 - app/proguard-rules.pro | 2 + .../fdroid/fdroid/localrepo/SwapService.java | 70 +++++++------------ .../fdroid/fdroid/views/swap/WifiQrView.java | 43 ++++++------ 4 files changed, 47 insertions(+), 69 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 35eacfea8..714fc10e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,6 @@ def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true")) android { compileSdkVersion 24 buildToolsVersion '25.0.3' - useLibrary 'org.apache.http.legacy' buildTypes { // use proguard on debug too since we have unknowingly broken diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 517c234ec..159f2ab7d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,6 +7,8 @@ -dontwarn com.android.support.test.** -dontwarn javax.naming.** +-dontnote org.apache.http.** +-dontnote android.net.http.** -dontnote android.support.** -dontnote **ILicensingService diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index 6f8060b9d..dafc33dc2 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.localrepo; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; @@ -10,7 +11,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; -import android.net.http.AndroidHttpClient; import android.os.AsyncTask; import android.os.IBinder; import android.support.annotation.IntDef; @@ -20,11 +20,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; -import org.apache.http.HttpHost; -import org.apache.http.NameValuePair; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.message.BasicNameValuePair; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; @@ -47,13 +42,14 @@ import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -208,55 +204,37 @@ public class SwapService extends Service { UpdateService.updateRepoNow(this, peer.getRepoAddress()); } + @SuppressLint("StaticFieldLeak") private void askServerToSwapWithUs(final Repo repo) { - askServerToSwapWithUs(repo.address); - } - - private void askServerToSwapWithUs(final String address) { new AsyncTask() { @Override protected Void doInBackground(Void... args) { - Uri repoUri = Uri.parse(address); String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString(); - - AndroidHttpClient client = AndroidHttpClient.newInstance("F-Droid", SwapService.this); - HttpPost request = new HttpPost("/request-swap"); - HttpHost host = new HttpHost(repoUri.getHost(), repoUri.getPort(), repoUri.getScheme()); - + HttpURLConnection conn = null; try { - Utils.debugLog(TAG, "Asking server at " + address + " to swap with us in return (by POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\")..."); - populatePostParams(swapBackUri, request); - client.execute(host, request); + URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap")); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoInput(true); + conn.setDoOutput(true); + + OutputStream outputStream = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(outputStream); + writer.write("repo=" + swapBackUri); + writer.flush(); + writer.close(); + outputStream.close(); + + int responseCode = conn.getResponseCode(); + Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " + + "POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode); } catch (IOException e) { - notifyOfErrorOnUiThread(); Log.e(TAG, "Error while asking server to swap with us", e); } finally { - client.close(); + conn.disconnect(); } 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() { - // TODO: Broadcast error message so that whoever wants to can display a relevant - // message in the UI. This service doesn't understand the concept of UI. - /*runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText( - SwapService.this, - R.string.swap_reciprocate_failed, - Toast.LENGTH_LONG - ).show(); - } - });*/ - } }.execute(); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/WifiQrView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/WifiQrView.java index dd73267c6..8ec198c8b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/WifiQrView.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/WifiQrView.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.LightingColorFilter; import android.net.Uri; +import android.os.Build; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; @@ -19,9 +20,6 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.ScrollView; import android.widget.TextView; - -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.QrGenAsyncTask; @@ -31,9 +29,8 @@ import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.views.swap.device.camera.CameraCharacteristicsChecker; -import java.net.URI; -import java.util.List; import java.util.Locale; +import java.util.Set; public class WifiQrView extends ScrollView implements SwapWorkflowActivity.InnerView { @@ -142,32 +139,34 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner ipAddressView.setText(buttonLabel); Uri sharingUri = Utils.getSharingUri(FDroidApp.repo); - String qrUriString = scheme + sharingUri.getHost(); + StringBuilder qrUrlBuilder = new StringBuilder(scheme); + qrUrlBuilder.append(sharingUri.getHost()); if (sharingUri.getPort() != 80) { - qrUriString += ":" + sharingUri.getPort(); + qrUrlBuilder.append(':'); + qrUrlBuilder.append(sharingUri.getPort()); } - qrUriString += sharingUri.getPath(); + qrUrlBuilder.append(sharingUri.getPath()); boolean first = true; - // Andorid provides an API for getting the query parameters and iterating over them: - // Uri.getQueryParameterNames() - // But it is only available on later Android versions. As such we use URLEncodedUtils instead. - List parameters = URLEncodedUtils.parse(URI.create(sharingUri.toString()), "UTF-8"); - for (NameValuePair parameter : parameters) { - if (!"ssid".equals(parameter.getName())) { - if (first) { - qrUriString += "?"; - first = false; - } else { - qrUriString += "&"; + if (Build.VERSION.SDK_INT > 10) { + Set names = sharingUri.getQueryParameterNames(); + for (String name : names) { + if (!"ssid".equals(name)) { + if (first) { + qrUrlBuilder.append('?'); + first = false; + } else { + qrUrlBuilder.append('&'); + } + qrUrlBuilder.append(name.toUpperCase(Locale.ENGLISH)); + qrUrlBuilder.append('='); + qrUrlBuilder.append(sharingUri.getQueryParameter(name).toUpperCase(Locale.ENGLISH)); } - qrUriString += parameter.getName().toUpperCase(Locale.ENGLISH) + "=" + - parameter.getValue().toUpperCase(Locale.ENGLISH); } } + String qrUriString = qrUrlBuilder.toString(); Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString); - new QrGenAsyncTask(getActivity(), R.id.wifi_qr_code).execute(qrUriString); } From 73ad5bd6b3bc5564cd09657401b68f08e7cb4d47 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 11 Apr 2018 14:04:11 +0200 Subject: [PATCH 04/11] update to latest jmDNS for more reliable detection closes #612 --- app/build.gradle | 5 +++-- app/proguard-rules.pro | 1 + .../javax/jmdns/impl/FDroidServiceInfo.java | 22 +++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 714fc10e5..c6699eda9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,7 +40,7 @@ dependencies { compile "info.guardianproject.panic:panic:0.5" compile 'commons-io:commons-io:2.5' compile 'commons-net:commons-net:3.5' - compile 'org.openhab.jmdns:jmdns:3.4.2' + compile 'org.jmdns:jmdns:3.5.3' compile 'ch.acra:acra:4.9.1' compile 'io.reactivex:rxjava:1.1.0' compile 'io.reactivex:rxandroid:0.23.0' @@ -137,7 +137,8 @@ if (!hasProperty('sourceDeps')) { 'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2', 'io.reactivex:rxandroid:35c1a90f8c1f499db3c1f3d608e1f191ac8afddb10c02dd91ef04c03a0a4bcda', 'io.reactivex:rxjava:2c162afd78eba217cdfee78b60e85d3bfb667db61e12bc95e3cf2ddc5beeadf6', - 'org.openhab.jmdns:jmdns:7a4b34b5606bbd2aff7fdfe629edcb0416fccd367fb59a099f210b9aba4f0bce', + 'org.jmdns:jmdns:24e7e3a50a579136400e8c9b0750399eb3c7558918bdf52c0ffa5e0fa5aad503', + 'org.slf4j:slf4j-api:e56288031f5e60652c06e7bb6e9fa410a61231ab54890f7b708fc6adc4107c5b', ] } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 159f2ab7d..06752191a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,6 +7,7 @@ -dontwarn com.android.support.test.** -dontwarn javax.naming.** +-dontwarn org.slf4j.** -dontnote org.apache.http.** -dontnote android.net.http.** -dontnote android.support.** diff --git a/app/src/main/java/javax/jmdns/impl/FDroidServiceInfo.java b/app/src/main/java/javax/jmdns/impl/FDroidServiceInfo.java index 2851cb2d1..2fed8a798 100644 --- a/app/src/main/java/javax/jmdns/impl/FDroidServiceInfo.java +++ b/app/src/main/java/javax/jmdns/impl/FDroidServiceInfo.java @@ -4,12 +4,12 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import javax.jmdns.ServiceInfo; +import javax.jmdns.impl.util.ByteWrangler; 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 @@ -32,7 +32,7 @@ public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable { if (data == null || data.length == 0) { return null; } - String fingerprint = this.readUTF(data, 0, data.length); + String fingerprint = ByteWrangler.readUTF(data, 0, data.length); if (TextUtils.isEmpty(fingerprint)) { return null; } @@ -51,14 +51,14 @@ public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable { public FDroidServiceInfo(Parcel in) { super( - in.readString(), - in.readString(), - in.readString(), - in.readInt(), - in.readInt(), - in.readInt(), - in.readByte() != 0, - readBytes(in) + in.readString(), + in.readString(), + in.readString(), + in.readInt(), + in.readInt(), + in.readInt(), + in.readByte() != 0, + readBytes(in) ); int addressCount = in.readInt(); From 57e4315e18fdf2ff48c9c8d76103b1023329ea24 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 11 Apr 2018 14:15:17 +0200 Subject: [PATCH 05/11] quit BluetoothServer when Bluetooth is disabled --- .../org/fdroid/fdroid/net/bluetooth/BluetoothServer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java index 7ba308a3f..86d8a987d 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java +++ b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java @@ -66,7 +66,7 @@ public class BluetoothServer extends Thread { public void run() { isRunning = true; - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); try { serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid()); @@ -83,6 +83,11 @@ public class BluetoothServer extends Thread { break; } + if (!adapter.isEnabled()) { + Utils.debugLog(TAG, "User disabled Bluetooth from outside, stopping."); + break; + } + try { BluetoothSocket clientSocket = serverSocket.accept(); if (clientSocket != null) { From 3fca7c7153743aaeee5172409e3542539b79381c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 11 Apr 2018 14:35:37 +0200 Subject: [PATCH 06/11] disable swap "visible" checkboxes by default, they get dynamically set If users click these fast, then its easy to get caught in a loop fighting the dynamic setting of them. --- app/src/main/res/layout/swap_blank.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/swap_blank.xml b/app/src/main/res/layout/swap_blank.xml index 6266be168..55493c486 100644 --- a/app/src/main/res/layout/swap_blank.xml +++ b/app/src/main/res/layout/swap_blank.xml @@ -58,7 +58,7 @@ @@ -114,7 +114,7 @@ From f90b030e76ddb03e00bc0d46977c01c1bae3936d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 11 Apr 2018 18:01:06 +0200 Subject: [PATCH 07/11] save and restore Bluetooth/WiFi swap state and previous state This should make swap remember if Bluetooth/WiFi was disabled when swapping started, then automatically disable it when swapping is done. This also makes swapping remember the swap "visibility" that the user set, and restore that when the user starts swapping again. There are logic bugs elsewhere in the whole thing that prevent this from always working, but the state should be set and stored properly. --- .../fdroid/fdroid/localrepo/SwapService.java | 124 ++++++++++-------- .../fdroid/localrepo/type/BluetoothSwap.java | 9 +- .../fdroid/views/swap/StartSwapView.java | 5 +- .../views/swap/SwapWorkflowActivity.java | 11 +- 4 files changed, 85 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index dafc33dc2..68511903e 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; +import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; @@ -11,6 +12,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; +import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.IBinder; import android.support.annotation.IntDef; @@ -63,10 +65,13 @@ import java.util.concurrent.ConcurrentHashMap; public class SwapService extends Service { private static final String TAG = "SwapService"; + private static final String SHARED_PREFERENCES = "swap-state"; private static final String KEY_APPS_TO_SWAP = "appsToSwap"; private static final String KEY_BLUETOOTH_ENABLED = "bluetoothEnabled"; private static final String KEY_WIFI_ENABLED = "wifiEnabled"; + private static final String KEY_BLUETOOTH_ENABLED_BEFORE_SWAP = "bluetoothEnabledBeforeSwap"; + private static final String KEY_WIFI_ENABLED_BEFORE_SWAP = "wifiEnabledBeforeSwap"; @NonNull private final Set appsToSwap = new HashSet<>(); @@ -76,6 +81,10 @@ public class SwapService extends Service { */ private static final ConcurrentHashMap INSTALLED_APPS = new ConcurrentHashMap<>(); + private static SharedPreferences swapPreferences; + private static BluetoothAdapter bluetoothAdapter; + private static WifiManager wifiManager; + public static void stop(Context context) { Intent intent = new Intent(context, SwapService.class); context.stopService(intent); @@ -89,16 +98,6 @@ public class SwapService extends Service { INSTALLED_APPS.put(packageName, app); } - /** - * Where relevant, the state of the swap process will be saved to disk using preferences. - * Note that this is not always useful, for example saving the "current wifi network" is - * bound to cause trouble when the user opens the swap process again and is connected to - * a different network. - */ - private SharedPreferences persistence() { - return getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); - } - // ========================================================== // Search for peers to swap // ========================================================== @@ -311,7 +310,7 @@ public class SwapService extends Service { // ========================================== private void persistAppsToSwap() { - persistence().edit().putString(KEY_APPS_TO_SWAP, serializePackages(appsToSwap)).apply(); + swapPreferences.edit().putString(KEY_APPS_TO_SWAP, serializePackages(appsToSwap)).apply(); } /** @@ -367,30 +366,36 @@ public class SwapService extends Service { persistAppsToSwap(); } - // ============================================================= - // Remember which swap technologies a user used in the past - // ============================================================= - - private final BroadcastReceiver receiveSwapStatusChanged = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Utils.debugLog(TAG, "Remembering that Bluetooth swap " + (bluetoothSwap.isConnected() ? "IS" : "is NOT") + - " connected and WiFi swap " + (wifiSwap.isConnected() ? "IS" : "is NOT") + " connected."); - persistence().edit() - .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) - .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) - .apply(); - } - }; - - /* - private boolean wasBluetoothEnabled() { - return persistence().getBoolean(KEY_BLUETOOTH_ENABLED, false); + public static boolean getBluetoothVisibleUserPreference() { + return swapPreferences.getBoolean(SwapService.KEY_BLUETOOTH_ENABLED, false); } - */ - private boolean wasWifiEnabled() { - return persistence().getBoolean(KEY_WIFI_ENABLED, false); + public static void putBluetoothVisibleUserPreference(boolean visible) { + swapPreferences.edit().putBoolean(SwapService.KEY_BLUETOOTH_ENABLED, visible).apply(); + } + + public static boolean getWifiVisibleUserPreference() { + return swapPreferences.getBoolean(SwapService.KEY_WIFI_ENABLED, false); + } + + public static void putWifiVisibleUserPreference(boolean visible) { + swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED, visible).apply(); + } + + public static boolean wasBluetoothEnabledBeforeSwap() { + return swapPreferences.getBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, false); + } + + public static void putBluetoothEnabledBeforeSwap(boolean visible) { + swapPreferences.edit().putBoolean(SwapService.KEY_BLUETOOTH_ENABLED_BEFORE_SWAP, visible).apply(); + } + + public static boolean wasWifiEnabledBeforeSwap() { + return swapPreferences.getBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, false); + } + + public static void putWifiEnabledBeforeSwap(boolean visible) { + swapPreferences.edit().putBoolean(SwapService.KEY_WIFI_ENABLED_BEFORE_SWAP, visible).apply(); } /** @@ -430,9 +435,6 @@ public class SwapService extends Service { return wifiSwap.isConnected() && wifiSwap.getBonjour().isConnected(); } - public static final String ACTION_PEER_FOUND = "org.fdroid.fdroid.SwapManager.ACTION_PEER_FOUND"; - public static final String EXTRA_PEER = "EXTRA_PEER"; - // =============================================================== // Old SwapService stuff being merged into that. // =============================================================== @@ -481,32 +483,39 @@ public class SwapService extends Service { CacheSwapAppsService.startCaching(this); - SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); + swapPreferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); - appsToSwap.addAll(deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, ""))); + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter != null) { + SwapService.putBluetoothEnabledBeforeSwap(bluetoothAdapter.isEnabled()); + } + + wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager != null) { + SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled()); + } + + appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, ""))); bluetoothSwap = BluetoothSwap.create(this); wifiSwap = new WifiSwap(this); Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); - LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); + LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, + new IntentFilter(WifiStateChangeService.BROADCAST)); - IntentFilter filter = new IntentFilter(BLUETOOTH_STATE_CHANGE); - filter.addAction(WIFI_STATE_CHANGE); - LocalBroadcastManager.getInstance(this).registerReceiver(receiveSwapStatusChanged, filter); - - /* - if (wasBluetoothEnabled()) { + if (getBluetoothVisibleUserPreference()) { Utils.debugLog(TAG, "Previously the user enabled Bluetooth swap, so enabling again automatically."); - bluetoothSwap.startInBackground(); - } - */ - - if (wasWifiEnabled()) { - Utils.debugLog(TAG, "Previously the user enabled WiFi swap, so enabling again automatically."); - wifiSwap.startInBackground(); + bluetoothSwap.startInBackground(); // TODO replace with Intent to SwapService } else { - Utils.debugLog(TAG, "WiFi was NOT enabled last time user swapped, so starting with WiFi not visible."); + Utils.debugLog(TAG, "Bluetooth was NOT enabled last time user swapped, starting not visible."); + } + + if (getWifiVisibleUserPreference()) { + Utils.debugLog(TAG, "Previously the user enabled WiFi swap, so enabling again automatically."); + wifiSwap.startInBackground(); // TODO replace with Intent to SwapService + } else { + Utils.debugLog(TAG, "WiFi was NOT enabled last time user swapped, starting not visible."); } } @@ -527,7 +536,14 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); - LocalBroadcastManager.getInstance(this).unregisterReceiver(receiveSwapStatusChanged); + + if (!SwapService.wasBluetoothEnabledBeforeSwap()) { + bluetoothAdapter.disable(); + } + + if (!SwapService.wasWifiEnabledBeforeSwap()) { + wifiManager.setWifiEnabled(false); + } //TODO getBluetoothSwap().stopInBackground(); getWifiSwap().stopInBackground(); diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java b/app/src/main/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java index b81a3fb6e..7ab9735b1 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/type/BluetoothSwap.java @@ -8,7 +8,6 @@ import android.content.IntentFilter; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; - import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.net.bluetooth.BluetoothServer; @@ -59,8 +58,8 @@ public final class BluetoothSwap extends SwapType { @Override public synchronized void start() { - if (isConnected()) { + Utils.debugLog(TAG, "already running, quitting start()"); return; } @@ -171,10 +170,12 @@ public final class BluetoothSwap extends SwapType { } @Override - public void start() { } + public void start() { + } @Override - public void stop() { } + public void stop() { + } @Override protected String getBroadcastAction() { diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/StartSwapView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/StartSwapView.java index 72270d387..1891468cd 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/StartSwapView.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/StartSwapView.java @@ -324,6 +324,7 @@ public class StartSwapView extends RelativeLayout implements SwapWorkflowActivit viewBluetoothId.setVisibility(View.GONE); Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully."); } + SwapService.putBluetoothVisibleUserPreference(isChecked); } }; @@ -344,7 +345,8 @@ public class StartSwapView extends RelativeLayout implements SwapWorkflowActivit // and the Bonjour service at the same time. Technically swap will work fine without // Bonjour, and that is more of a convenience. Thus, we should show feedback once wifi // is ready, even if Bonjour is not yet. - LocalBroadcastManager.getInstance(getContext()).registerReceiver(onWifiSwapStateChanged, new IntentFilter(SwapService.WIFI_STATE_CHANGE)); + LocalBroadcastManager.getInstance(getContext()).registerReceiver(onWifiSwapStateChanged, + new IntentFilter(SwapService.WIFI_STATE_CHANGE)); viewWifiNetwork.setOnClickListener(new OnClickListener() { @Override @@ -426,6 +428,7 @@ public class StartSwapView extends RelativeLayout implements SwapWorkflowActivit Utils.debugLog(TAG, "Received onCheckChanged(false) for WiFi swap, disabling WiFi swap in background thread."); getManager().getWifiSwap().stopInBackground(); } + SwapService.putWifiVisibleUserPreference(isChecked); uiUpdateWifiNetwork(); } }; diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index f889ca834..ac0b43c8d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -574,17 +574,18 @@ public class SwapWorkflowActivity extends AppCompatActivity { Utils.debugLog(TAG, "User enabled Bluetooth, will make sure we are discoverable."); ensureBluetoothDiscoverableThenStart(); } else { - // Didn't enable bluetooth - Utils.debugLog(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi)."); + Utils.debugLog(TAG, "User chose not to enable Bluetooth, so doing nothing"); + SwapService.putBluetoothVisibleUserPreference(false); } } else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) { if (resultCode != RESULT_CANCELED) { Utils.debugLog(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server."); - getState().getBluetoothSwap().startInBackground(); + getState().getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService } else { - Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi)."); + Utils.debugLog(TAG, "User chose not to make Bluetooth discoverable, so doing nothing"); + SwapService.putBluetoothVisibleUserPreference(false); } } else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SEND) { @@ -640,7 +641,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { throw new IllegalStateException("Can't start Bluetooth swap because service is null for some strange reason."); } - service.getBluetoothSwap().startInBackground(); + service.getBluetoothSwap().startInBackground(); // TODO replace with Intent to SwapService } class PrepareInitialSwapRepo extends PrepareSwapRepo { From 570b532bd61d8cc87f41f4cbe876e93f12c8f4a3 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 12 Apr 2018 12:04:42 +0200 Subject: [PATCH 08/11] prompt user for WRITE_SETTINGS permission when setting up Hotspot closes #656 --- .../views/swap/SwapWorkflowActivity.java | 75 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 + 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index ac0b43c8d..3ca24235b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.views.swap; +import android.annotation.TargetApi; import android.app.Activity; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; @@ -12,8 +13,10 @@ import android.content.ServiceConnection; import android.net.Uri; import android.net.wifi.WifiManager; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.provider.Settings; import android.support.annotation.ColorRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; @@ -110,6 +113,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SWAP = 2; private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3; private static final int REQUEST_BLUETOOTH_ENABLE_FOR_SEND = 4; + private static final int REQUEST_WRITE_SETTINGS_PERMISSION = 5; private Toolbar toolbar; private InnerView currentView; @@ -250,32 +254,37 @@ public class SwapWorkflowActivity extends AppCompatActivity { public void onClick(DialogInterface dialog, int which) { // Do nothing } - } - ).setPositiveButton(R.string.wifi, new DialogInterface.OnClickListener() { + }) + .setPositiveButton(R.string.wifi, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)); } - } - ).setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() { + }) + .setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - promptToSetupWifiAP(); + if (Build.VERSION.SDK_INT >= 26) { + showTetheringSettings(); + } else if (Build.VERSION.SDK_INT >= 23 && !Settings.System.canWrite(getBaseContext())) { + requestWriteSettingsPermission(); + } else { + setupWifiAP(); + } } - } - ).create().show(); + }) + .create().show(); } - private void promptToSetupWifiAP() { + private void setupWifiAP() { WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiApControl ap = WifiApControl.getInstance(this); wifiManager.setWifiEnabled(false); - if (!ap.enable()) { - Log.e(TAG, "Could not enable WiFi AP."); - // TODO: Feedback to user? + if (ap.enable()) { + Toast.makeText(this, R.string.swap_toast_hotspot_enabled, Toast.LENGTH_SHORT).show(); } else { - Utils.debugLog(TAG, "WiFi AP enabled."); - // TODO: Seems to be broken some times... + Toast.makeText(this, R.string.swap_toast_could_not_enable_hotspot, Toast.LENGTH_LONG).show(); + Log.e(TAG, "Could not enable WiFi AP."); } } @@ -419,6 +428,29 @@ public class SwapWorkflowActivity extends AppCompatActivity { inflateInnerView(R.layout.swap_select_apps); } + /** + * On {@code android-26}, only apps with privileges can access + * {@code WRITE_SETTINGS}. So this just shows the tethering settings + * for the user to do it themselves. + */ + public void showTetheringSettings() { + final Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + final ComponentName cn = new ComponentName("com.android.settings", + "com.android.settings.TetherSettings"); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + @TargetApi(23) + public void requestWriteSettingsPermission() { + Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, + Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityForResult(intent, REQUEST_WRITE_SETTINGS_PERMISSION); + } + public void sendFDroid() { // If Bluetooth has not been enabled/turned on, then enabling device discoverability // will automatically enable Bluetooth. @@ -568,6 +600,10 @@ public class SwapWorkflowActivity extends AppCompatActivity { } } else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) { finish(); + } else if (requestCode == REQUEST_WRITE_SETTINGS_PERMISSION) { + if (Build.VERSION.SDK_INT >= 23 && Settings.System.canWrite(this)) { + setupWifiAP(); + } } else if (requestCode == REQUEST_BLUETOOTH_ENABLE_FOR_SWAP) { if (resultCode == RESULT_OK) { @@ -595,12 +631,13 @@ public class SwapWorkflowActivity extends AppCompatActivity { /** * The process for setting up bluetooth is as follows: - * * Assume we have bluetooth available (otherwise the button which allowed us to start - * the bluetooth process should not have been available). - * * Ask user to enable (if not enabled yet). - * * Start bluetooth server socket. - * * Enable bluetooth discoverability, so that people can connect to our server socket. - * + *
    + *
  • Assume we have bluetooth available (otherwise the button which allowed us to start + * the bluetooth process should not have been available)
  • + *
  • Ask user to enable (if not enabled yet)
  • + *
  • Start bluetooth server socket
  • + *
  • Enable bluetooth discoverability, so that people can connect to our server socket.
  • + *
* Note that this is a little different than the usual process for bluetooth _clients_, which * involves pairing and connecting with other devices. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24456d40f..c320b08f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -461,6 +461,8 @@ This often occurs with apps installed via Google Play or other sources, if they Swapping not enabled Before swapping, your device must be made visible. Invalid URL for swapping: %1$s + Wi-Fi Hotspot enabled + Could not enable Wi-Fi Hotspot! needs access to Do you want to install an update From 444ecd024b134839cd262c5ecdecd2ea1640bfdc Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 11 Apr 2018 18:05:04 +0200 Subject: [PATCH 09/11] when user turns on swap WiFi visibility, ensure that WiFi is on --- .../main/java/org/fdroid/fdroid/localrepo/SwapService.java | 2 +- .../java/org/fdroid/fdroid/localrepo/type/WifiSwap.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index 68511903e..b7e509179 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -497,7 +497,7 @@ public class SwapService extends Service { appsToSwap.addAll(deserializePackages(swapPreferences.getString(KEY_APPS_TO_SWAP, ""))); bluetoothSwap = BluetoothSwap.create(this); - wifiSwap = new WifiSwap(this); + wifiSwap = new WifiSwap(this, wifiManager); Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java b/app/src/main/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java index 152877aec..f76dbe042 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/type/WifiSwap.java @@ -2,6 +2,7 @@ package org.fdroid.fdroid.localrepo.type; import android.annotation.SuppressLint; import android.content.Context; +import android.net.wifi.WifiManager; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -31,10 +32,12 @@ public class WifiSwap extends SwapType { private Handler webServerThreadHandler; private LocalHTTPD localHttpd; private final BonjourBroadcast bonjourBroadcast; + private final WifiManager wifiManager; - public WifiSwap(Context context) { + public WifiSwap(Context context, WifiManager wifiManager) { super(context); bonjourBroadcast = new BonjourBroadcast(context); + this.wifiManager = wifiManager; } protected String getBroadcastAction() { @@ -47,6 +50,8 @@ public class WifiSwap extends SwapType { @Override public void start() { + wifiManager.setWifiEnabled(true); + Utils.debugLog(TAG, "Preparing swap webserver."); sendBroadcast(SwapService.EXTRA_STARTING); From f07e5c040cc56e736818f2f8754054a0b9cd37b0 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 12 Apr 2018 12:58:28 +0200 Subject: [PATCH 10/11] when user selects WiFi over Hotspot, make sure WiFi is enabled --- .../fdroid/fdroid/views/swap/SwapWorkflowActivity.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 3ca24235b..1a9a3ce3a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -121,6 +121,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { private PrepareSwapRepo updateSwappableAppsTask; private NewRepoConfig confirmSwapConfig; private LocalBroadcastManager localBroadcastManager; + private WifiManager wifiManager; @NonNull private final ServiceConnection serviceConnection = new ServiceConnection() { @@ -186,6 +187,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { container = (ViewGroup) findViewById(R.id.fragment_container); localBroadcastManager = LocalBroadcastManager.getInstance(this); + wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); new SwapDebug().logStatus(); } @@ -258,7 +260,11 @@ public class SwapWorkflowActivity extends AppCompatActivity { .setPositiveButton(R.string.wifi, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)); + SwapService.putWifiEnabledBeforeSwap(wifiManager.isWifiEnabled()); + wifiManager.setWifiEnabled(true); + Intent intent = new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); } }) .setNegativeButton(R.string.wifi_ap, new DialogInterface.OnClickListener() { @@ -277,7 +283,6 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void setupWifiAP() { - WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiApControl ap = WifiApControl.getInstance(this); wifiManager.setWifiEnabled(false); if (ap.enable()) { From c770d4ef18ff88f2a4afa804145582611c9bcb86 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 13 Apr 2018 00:26:54 +0200 Subject: [PATCH 11/11] prefer WiFi in Send F-Droid when the conditions dictate that --- .../fdroid/views/swap/SendFDroidView.java | 149 ++++++++++++++++++ .../views/swap/SwapWorkflowActivity.java | 48 +++--- app/src/main/res/layout/swap_send_fdroid.xml | 50 ++++++ app/src/main/res/values-af/strings.xml | 4 - app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values-ast/strings.xml | 4 - app/src/main/res/values-be/strings.xml | 4 - app/src/main/res/values-bg/strings.xml | 4 - app/src/main/res/values-bo/strings.xml | 2 - app/src/main/res/values-ca/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 4 - app/src/main/res/values-da/strings.xml | 3 - app/src/main/res/values-de/strings.xml | 4 - app/src/main/res/values-el/strings.xml | 4 - app/src/main/res/values-eo/strings.xml | 4 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-et/strings.xml | 3 - app/src/main/res/values-eu/strings.xml | 4 - app/src/main/res/values-fa/strings.xml | 2 - app/src/main/res/values-fi/strings.xml | 4 - app/src/main/res/values-fr/strings.xml | 4 - app/src/main/res/values-gl/strings.xml | 4 - app/src/main/res/values-he/strings.xml | 2 - app/src/main/res/values-hi/strings.xml | 3 - app/src/main/res/values-hr/strings.xml | 3 - app/src/main/res/values-hu/strings.xml | 4 - app/src/main/res/values-id/strings.xml | 4 - app/src/main/res/values-is/strings.xml | 3 - app/src/main/res/values-it/strings.xml | 4 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-kab/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 2 - app/src/main/res/values-ml/strings.xml | 2 - app/src/main/res/values-my/strings.xml | 1 - app/src/main/res/values-nb/strings.xml | 4 - app/src/main/res/values-nl/strings.xml | 4 - app/src/main/res/values-pl/strings.xml | 4 - app/src/main/res/values-pt-rBR/strings.xml | 4 - app/src/main/res/values-pt-rPT/strings.xml | 4 - app/src/main/res/values-ro/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-sc/strings.xml | 4 - app/src/main/res/values-sk/strings.xml | 4 - app/src/main/res/values-sn/strings.xml | 4 - app/src/main/res/values-sr/strings.xml | 3 - app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-th/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-uk/strings.xml | 4 - app/src/main/res/values-vi/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values-zh-rHK/strings.xml | 2 - app/src/main/res/values-zh-rTW/strings.xml | 2 - app/src/main/res/values/strings.xml | 4 +- 54 files changed, 226 insertions(+), 177 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/views/swap/SendFDroidView.java create mode 100644 app/src/main/res/layout/swap_send_fdroid.xml diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SendFDroidView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SendFDroidView.java new file mode 100644 index 000000000..60bf9fc02 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SendFDroidView.java @@ -0,0 +1,149 @@ +package org.fdroid.fdroid.views.swap; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.LightingColorFilter; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.TextView; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.QrGenAsyncTask; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.localrepo.SwapService; +import org.fdroid.fdroid.net.WifiStateChangeService; +import org.fdroid.fdroid.views.swap.device.camera.CameraCharacteristicsChecker; + +public class SendFDroidView extends ScrollView implements SwapWorkflowActivity.InnerView { + + private static final String TAG = "SendFDroidView"; + + public SendFDroidView(Context context) { + super(context); + } + + public SendFDroidView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SendFDroidView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(21) + public SendFDroidView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private SwapWorkflowActivity getActivity() { + return (SwapWorkflowActivity) getContext(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setUIFromWifi(); + setUpWarningMessageQrScan(); + + ImageView qrImage = (ImageView) findViewById(R.id.wifi_qr_code); + + // Replace all blacks with the background blue. + qrImage.setColorFilter(new LightingColorFilter(0xffffffff, getResources().getColor(R.color.swap_blue))); + + Button useBluetooth = (Button) findViewById(R.id.btn_use_bluetooth); + useBluetooth.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().showIntro(); + getActivity().sendFDroidBluetooth(); + } + }); + + LocalBroadcastManager.getInstance(getActivity()).registerReceiver( + onWifiStateChanged, new IntentFilter(WifiStateChangeService.BROADCAST)); + } + + private void setUpWarningMessageQrScan() { + final View qrWarningMessage = findViewById(R.id.warning_qr_scanner); + final boolean hasAutofocus = CameraCharacteristicsChecker.getInstance(getContext()).hasAutofocus(); + final int visiblity = hasAutofocus ? GONE : VISIBLE; + qrWarningMessage.setVisibility(visiblity); + } + + + /** + * Remove relevant listeners/receivers/etc so that they do not receive and process events + * when this view is not in use. + */ + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(onWifiStateChanged); + } + + @Override + public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) { + return false; + } + + @Override + public int getStep() { + return SwapService.STEP_INTRO; + } + + @Override + public int getPreviousStep() { + return SwapService.STEP_INTRO; + } + + @ColorRes + public int getToolbarColour() { + return R.color.swap_blue; + } + + @Override + public String getToolbarTitle() { + return getResources().getString(R.string.swap_send_fdroid); + } + + @SuppressLint("HardwareIds") + private void setUIFromWifi() { + if (TextUtils.isEmpty(FDroidApp.repo.address)) { + return; + } + + String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https://" : "http://"; + + // the fingerprint is not useful on the button label + String qrUriString = scheme + FDroidApp.ipAddressString + ":" + FDroidApp.port; + TextView ipAddressView = (TextView) findViewById(R.id.device_ip_address); + ipAddressView.setText(qrUriString); + + Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString); + new QrGenAsyncTask(getActivity(), R.id.wifi_qr_code).execute(qrUriString); + + } + + private final BroadcastReceiver onWifiStateChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + setUIFromWifi(); + } + }; + +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 1a9a3ce3a..6098a58cd 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -202,7 +202,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); boolean parent = super.onPrepareOptionsMenu(menu); - boolean inner = currentView != null && currentView.buildMenu(menu, getMenuInflater()); + boolean inner = currentView != null && currentView.buildMenu(menu, getMenuInflater()); return parent || inner; } @@ -457,28 +457,28 @@ public class SwapWorkflowActivity extends AppCompatActivity { } public void sendFDroid() { - // If Bluetooth has not been enabled/turned on, then enabling device discoverability - // will automatically enable Bluetooth. BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - if (adapter.getState() != BluetoothAdapter.STATE_ON) { - Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); - discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120); - startActivityForResult(discoverBt, REQUEST_BLUETOOTH_ENABLE_FOR_SEND); - } else { - sendFDroidApk(); - } + if (adapter == null + || Build.VERSION.SDK_INT >= 23 // TODO make Bluetooth work with content:// URIs + || (!adapter.isEnabled() && getService().getWifiSwap().isConnected())) { + showSendFDroid(); } else { - new AlertDialog.Builder(this) - .setTitle(R.string.bluetooth_unavailable) - .setMessage(R.string.swap_cant_send_no_bluetooth) - .setNegativeButton( - R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { } - } - ).create().show(); + sendFDroidBluetooth(); + } + } + + /** + * Send the F-Droid APK via Bluetooth. If Bluetooth has not been + * enabled/turned on, then enabling device discoverability will + * automatically enable Bluetooth. + */ + public void sendFDroidBluetooth() { + if (BluetoothAdapter.getDefaultAdapter().isEnabled()) { + sendFDroidApk(); + } else { + Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120); + startActivityForResult(discoverBt, REQUEST_BLUETOOTH_ENABLE_FOR_SEND); } } @@ -531,6 +531,10 @@ public class SwapWorkflowActivity extends AppCompatActivity { inflateInnerView(R.layout.swap_wifi_qr); } + public void showSendFDroid() { + inflateInnerView(R.layout.swap_send_fdroid); + } + public void showSwapConnected() { inflateInnerView(R.layout.swap_success); } @@ -688,7 +692,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { class PrepareInitialSwapRepo extends PrepareSwapRepo { PrepareInitialSwapRepo() { - super(new HashSet<>(Arrays.asList(new String[] {BuildConfig.APPLICATION_ID}))); + super(new HashSet<>(Arrays.asList(new String[]{BuildConfig.APPLICATION_ID}))); } } diff --git a/app/src/main/res/layout/swap_send_fdroid.xml b/app/src/main/res/layout/swap_send_fdroid.xml new file mode 100644 index 000000000..79f1736e5 --- /dev/null +++ b/app/src/main/res/layout/swap_send_fdroid.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + +