diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 2ce145ddd..60c1322e0 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -301,6 +301,7 @@ Learn more about Wifi Swap apps Swap apps + Swap No network yet (Tap to open available networks) It\'s not working @@ -309,4 +310,5 @@ Do you want to get apps from %1$s now? Your mobile device becomes an app store with Swap! START A SWAP + An error occurred while attempting to swap with another device. We will still try to get apps from them, but they may not be able to get apps from us. diff --git a/F-Droid/src/org/fdroid/fdroid/FDroid.java b/F-Droid/src/org/fdroid/fdroid/FDroid.java index 1b29bdd51..607ce98c9 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroid.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroid.java @@ -198,7 +198,9 @@ public class FDroid extends ActionBarActivity { if (parser.isValidRepo()) { intent.putExtra("handled", true); if (parser.isFromSwap()) { - startActivityForResult(new Intent(ACTION_ADD_REPO, intent.getData(), this, ConnectSwapActivity.class), REQUEST_SWAP); + Intent confirmIntent = new Intent(this, ConnectSwapActivity.class); + confirmIntent.setData(intent.getData()); + startActivityForResult(confirmIntent, REQUEST_SWAP); } else { startActivity(new Intent(ACTION_ADD_REPO, intent.getData(), this, ManageReposActivity.class)); } diff --git a/F-Droid/src/org/fdroid/fdroid/Utils.java b/F-Droid/src/org/fdroid/fdroid/Utils.java index 924568bf0..c7e7c7bd2 100644 --- a/F-Droid/src/org/fdroid/fdroid/Utils.java +++ b/F-Droid/src/org/fdroid/fdroid/Utils.java @@ -285,14 +285,25 @@ public final class Utils { } @NonNull + public static Uri getLocalRepoUri(Repo repo) { + if (TextUtils.isEmpty(repo.address)) + return Uri.parse("http://wifi-not-enabled"); + Uri uri = Uri.parse(repo.address); + Uri.Builder b = uri.buildUpon(); + if (!TextUtils.isEmpty(repo.fingerprint)) + b.appendQueryParameter("fingerprint", repo.fingerprint); + String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https" : "http"; + b.scheme(scheme); + return b.build(); + } + public static Uri getSharingUri(Repo repo) { if (TextUtils.isEmpty(repo.address)) return Uri.parse("http://wifi-not-enabled"); - Uri uri = Uri.parse(repo.address.replaceFirst("http", "fdroidrepo")); - Uri.Builder b = uri.buildUpon(); + Uri localRepoUri = getLocalRepoUri(repo); + Uri.Builder b = localRepoUri.buildUpon(); + b.scheme(localRepoUri.getScheme().replaceFirst("http", "fdroidrepo")); b.appendQueryParameter("swap", "1"); - if (!TextUtils.isEmpty(repo.fingerprint)) - b.appendQueryParameter("fingerprint", repo.fingerprint); if (!TextUtils.isEmpty(FDroidApp.bssid)) { b.appendQueryParameter("bssid", Uri.encode(FDroidApp.bssid)); if (!TextUtils.isEmpty(FDroidApp.ssid)) diff --git a/F-Droid/src/org/fdroid/fdroid/data/NewRepoConfig.java b/F-Droid/src/org/fdroid/fdroid/data/NewRepoConfig.java index 6e41123ed..e55d8c94f 100644 --- a/F-Droid/src/org/fdroid/fdroid/data/NewRepoConfig.java +++ b/F-Droid/src/org/fdroid/fdroid/data/NewRepoConfig.java @@ -7,6 +7,7 @@ import android.text.TextUtils; import android.util.Log; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.views.swap.ConnectSwapActivity; import java.util.Arrays; import java.util.Locale; @@ -27,6 +28,7 @@ public class NewRepoConfig { private String bssid; private String ssid; private boolean fromSwap; + private boolean preventFurtherSwaps; public NewRepoConfig(Context context, String uri) { init(context, uri != null ? Uri.parse(uri) : null); @@ -34,6 +36,7 @@ public class NewRepoConfig { public NewRepoConfig(Context context, Intent intent) { init(context, intent.getData()); + preventFurtherSwaps = intent.getBooleanExtra(ConnectSwapActivity.EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, false); } private void init(Context context, Uri incomingUri) { @@ -110,14 +113,28 @@ public class NewRepoConfig { return port; } - public String getUriString() { + public String getRepoUriString() { return uriString; } - public Uri getUri() { + /** + * This is the URI which was passed to the NewRepoConfig for parsing. + * Not that it may be an fdroidrepo:// or http:// scheme, and it may also have + * ssid, bssid, and perhaps other query parameters. If you want the actual repo + * URL, then you will probably want {@link org.fdroid.fdroid.data.NewRepoConfig#getRepoUri()}. + */ + public Uri getParsedUri() { return uri; } + public Uri getRepoUri() { + if (uriString == null) { + return null; + } else { + return Uri.parse(uriString); + } + } + public String getHost() { return host; } @@ -138,6 +155,10 @@ public class NewRepoConfig { return fromSwap; } + public boolean preventFurtherSwaps() { + return preventFurtherSwaps; + } + /* * The port starts out as 8888, but if there is a conflict, it will be * incremented until there is a free port found. diff --git a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java index 193f3625e..f3fed2679 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java +++ b/F-Droid/src/org/fdroid/fdroid/net/LocalHTTPD.java @@ -1,11 +1,14 @@ package org.fdroid.fdroid.net; import android.content.Context; +import android.content.Intent; +import android.net.Uri; import android.util.Log; import android.webkit.MimeTypeMap; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.localrepo.LocalRepoKeyStore; +import org.fdroid.fdroid.views.swap.ConnectSwapActivity; import java.io.File; import java.io.FileInputStream; @@ -19,6 +22,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.HashMap; import java.util.StringTokenizer; import javax.net.ssl.SSLServerSocketFactory; @@ -68,8 +72,54 @@ public class LocalHTTPD extends NanoHTTPD { return newUri; } + private void requestSwap(String repo) { + Log.d(TAG, "Received request to swap with " + repo); + Log.d(TAG, "Showing confirm screen to check whether that is okay with the user."); + + Uri repoUri = Uri.parse(repo); + Intent intent = new Intent(context, ConnectSwapActivity.class); + intent.setData(repoUri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(ConnectSwapActivity.EXTRA_PREVENT_FURTHER_SWAP_REQUESTS, true); + context.startActivity(intent); + } + @Override public Response serve(IHTTPSession session) { + + if (session.getMethod() == Method.POST) { + try { + session.parseBody(new HashMap()); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Internal server error, check logcat on server for details."); + } catch (ResponseException re) { + return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage()); + } + + return handlePost(session); + } else { + return handleGet(session); + } + } + + private Response handlePost(IHTTPSession session) { + Uri uri = Uri.parse(session.getUri()); + switch(uri.getPath()) { + case "/request-swap": + if (!session.getParms().containsKey("repo")) { + Log.e(TAG, "Malformed /request-swap request to local repo HTTP server. Should have posted a 'repo' parameter." ); + return new Response(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Requires 'repo' parameter to be posted."); + } else { + requestSwap(session.getParms().get("repo")); + return new Response(Response.Status.OK, MIME_PLAINTEXT, "Swap request received."); + } + } + return new Response(""); + } + + private Response handleGet(IHTTPSession session) { + Map header = session.getHeaders(); Map parms = session.getParms(); String uri = session.getUri(); diff --git a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java index fb6ad1148..097a5bfc9 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/ManageReposActivity.java @@ -584,7 +584,7 @@ public class ManageReposActivity extends ActionBarActivity { /* an URL from a click, NFC, QRCode scan, etc */ NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent); if (newRepoConfig.isValidRepo()) { - importRepo(newRepoConfig.getUriString(), newRepoConfig.getFingerprint()); + importRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint()); checkIfNewRepoOnSameWifi(newRepoConfig); } else if (newRepoConfig.getErrorMessage() != null) { Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show(); diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java index e5da26df8..353cff0f2 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceiveSwapFragment.java @@ -3,6 +3,8 @@ package org.fdroid.fdroid.views.swap; import android.app.Activity; import android.content.ContentValues; import android.net.Uri; +import android.net.http.AndroidHttpClient; +import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -13,13 +15,22 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import org.fdroid.fdroid.ProgressListener; -import org.fdroid.fdroid.R; -import org.fdroid.fdroid.UpdateService; +import android.widget.Toast; +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.*; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + public class ConfirmReceiveSwapFragment extends Fragment implements ProgressListener { private static final String TAG = "fdroid.ConfirmReceiveSwapFragment"; @@ -31,23 +42,19 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.swap_confirm_receive, container, false); - view.findViewById(R.id.no_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); - view.findViewById(R.id.yes_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { confirm(); } }); - return view; } @@ -65,26 +72,34 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList ); } else { // TODO: Show error message on screen (not in popup). + // TODO: I don't think we want to continue with this at all if the repo config is invalid, + // how should we boot the user from this screen in this case? } } private void confirm() { - this.repo = ensureRepoExists(); - UpdateService.updateRepoNow(this.repo.address, getActivity()).setListener(this); + repo = ensureRepoExists(); + if (repo != null) { + UpdateService.updateRepoNow(repo.address, getActivity()).setListener(this); + } } @NonNull private Repo ensureRepoExists() { - // TODO: newRepoConfig.getUri() will include a fingerprint, which may not match with - // the repos address in the database. - Repo repo = RepoProvider.Helper.findByAddress(getActivity(), newRepoConfig.getUriString()); + if (!newRepoConfig.isValidRepo()) { + return null; + } + + // TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with + // the repos address in the database. Not sure on best behaviour in this situation. + Repo repo = RepoProvider.Helper.findByAddress(getActivity(), newRepoConfig.getRepoUriString()); if (repo == null) { ContentValues values = new ContentValues(6); // TODO: i18n and think about most appropriate name. Although it wont be visible in // the "Manage repos" UI after being marked as a swap repo here... - values.put(RepoProvider.DataColumns.NAME, "Swap"); - values.put(RepoProvider.DataColumns.ADDRESS, newRepoConfig.getUriString()); + values.put(RepoProvider.DataColumns.NAME, getString(R.string.swap_repo_name)); + values.put(RepoProvider.DataColumns.ADDRESS, newRepoConfig.getRepoUriString()); values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO; values.put(RepoProvider.DataColumns.FINGERPRINT, newRepoConfig.getFingerprint()); values.put(RepoProvider.DataColumns.IN_USE, true); @@ -97,9 +112,64 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList values.put(RepoProvider.DataColumns.IS_SWAP, true); RepoProvider.Helper.update(getActivity(), repo, values); } + + if (!newRepoConfig.preventFurtherSwaps()) { + askServerToSwapWithUs(); + } + return repo; } + private void askServerToSwapWithUs() { + if (!newRepoConfig.isValidRepo()) { + return; + } + + new AsyncTask() { + @Override + protected Void doInBackground(Void... args) { + Uri repoUri = newRepoConfig.getRepoUri(); + String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString(); + + AndroidHttpClient client = AndroidHttpClient.newInstance("F-Droid", getActivity()); + HttpPost request = new HttpPost("/request-swap"); + HttpHost host = new HttpHost(repoUri.getHost(), repoUri.getPort(), repoUri.getScheme()); + + try { + Log.d(TAG, "Asking server at " + newRepoConfig.getRepoUriString() + " to swap with us in return (by POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\")..."); + populatePostParams(swapBackUri, request); + client.execute(host, request); + } catch (IOException e) { + notifyOfErrorOnUiThread(); + Log.e(TAG, "Error while asking server to swap with us: " + e.getMessage()); + } finally { + client.close(); + } + 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() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText( + getActivity(), + getString(R.string.swap_reciprocate_failed), + Toast.LENGTH_LONG + ).show(); + } + }); + } + }.execute(); + } + @Override public void onProgress(Event event) { // TODO: Show progress, but we can worry about that later. @@ -123,4 +193,8 @@ public class ConfirmReceiveSwapFragment extends Fragment implements ProgressList break; } } + + public void setRepoConfig(NewRepoConfig repoConfig) { + this.newRepoConfig = repoConfig; + } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java index 946cd3e77..642408856 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java @@ -4,6 +4,7 @@ import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; +import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.Repo; @@ -11,6 +12,17 @@ public class ConnectSwapActivity extends FragmentActivity { private static final String STATE_CONFIRM = "startSwap"; + /** + * When connecting to a swap, we then go and initiate a connection with that + * device and ask if it would like to swap with us. Upon receiving that request + * and agreeing, we don't then want to be asked whether we want to swap back. + * This flag protects against two devices continually going back and forth + * among each other offering swaps. + */ + public static final String EXTRA_PREVENT_FURTHER_SWAP_REQUESTS = "preventFurtherSwap"; + + private ConfirmReceiveSwapFragment fragment; + @Override public void onCreate(Bundle savedInstanceState) { @@ -18,9 +30,11 @@ public class ConnectSwapActivity extends FragmentActivity { if (savedInstanceState == null) { + fragment = new ConfirmReceiveSwapFragment(); + getSupportFragmentManager() .beginTransaction() - .replace(android.R.id.content, new ConfirmReceiveSwapFragment(), STATE_CONFIRM) + .replace(android.R.id.content, fragment, STATE_CONFIRM) .addToBackStack(STATE_CONFIRM) .commit(); @@ -28,6 +42,14 @@ public class ConnectSwapActivity extends FragmentActivity { } + @Override + protected void onResume() { + super.onResume(); + // Only confirm the action, and then return a result... + NewRepoConfig config = new NewRepoConfig(this, getIntent()); + fragment.setRepoConfig(config); + } + @Override public void onBackPressed() { if (currentState().equals(STATE_CONFIRM)) {