From 0b1e2732c23ccb3ea375463e8f04edc4cd348012 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Thu, 9 Jul 2015 11:40:28 +1000 Subject: [PATCH] Refactored SwapService + SwapManager into single SwapService --- F-Droid/res/layout/swap_activity.xml | 18 +- F-Droid/res/layout/swap_initial_loading.xml | 26 + F-Droid/res/values/strings.xml | 1 + .../fdroid/localrepo/LocalRepoManager.java | 2 +- .../fdroid/fdroid/localrepo/SwapManager.java | 461 --------------- .../fdroid/fdroid/localrepo/SwapService.java | 553 ++++++++++++++---- .../fdroid/localrepo/peers/PeerFinder.java | 9 +- .../fdroid/localrepo/type/BonjourType.java | 1 - .../fdroid/net/WifiStateChangeService.java | 16 +- .../fdroid/views/swap/ConfirmReceive.java | 13 +- .../views/swap/ConnectSwapActivity.java | 40 +- .../fdroid/views/swap/InitialLoadingView.java | 68 +++ .../fdroid/views/swap/JoinWifiView.java | 6 +- .../org/fdroid/fdroid/views/swap/NfcView.java | 6 +- .../fdroid/views/swap/SelectAppsView.java | 8 +- .../fdroid/views/swap/StartSwapView.java | 19 +- .../fdroid/views/swap/SwapAppsView.java | 9 +- .../fdroid/views/swap/SwapConnecting.java | 17 +- .../views/swap/SwapWorkflowActivity.java | 121 +++- .../fdroid/fdroid/views/swap/WifiQrView.java | 6 +- 20 files changed, 729 insertions(+), 671 deletions(-) create mode 100644 F-Droid/res/layout/swap_initial_loading.xml delete mode 100644 F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java create mode 100644 F-Droid/src/org/fdroid/fdroid/views/swap/InitialLoadingView.java diff --git a/F-Droid/res/layout/swap_activity.xml b/F-Droid/res/layout/swap_activity.xml index 8349507a5..9551080ab 100644 --- a/F-Droid/res/layout/swap_activity.xml +++ b/F-Droid/res/layout/swap_activity.xml @@ -6,16 +6,16 @@ android:orientation="vertical"> + android:id="@+id/toolbar" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:titleTextAppearance="@style/SwapTheme.Wizard.Text" + android:minHeight="?attr/actionBarSize" + android:background="?attr/colorPrimary" /> + android:id="@+id/fragment_container" + android:layout_width="fill_parent" + android:layout_height="fill_parent" /> \ No newline at end of file diff --git a/F-Droid/res/layout/swap_initial_loading.xml b/F-Droid/res/layout/swap_initial_loading.xml new file mode 100644 index 000000000..d1e169875 --- /dev/null +++ b/F-Droid/res/layout/swap_initial_loading.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/F-Droid/res/values/strings.xml b/F-Droid/res/values/strings.xml index 8966e549c..0d279f21f 100644 --- a/F-Droid/res/values/strings.xml +++ b/F-Droid/res/values/strings.xml @@ -347,4 +347,5 @@ May work Promising Best bet + Loading... diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java index e18f58e66..c43e61bb7 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -56,7 +56,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; /** - * The {@link SwapManager} deals with managing the entire workflow from selecting apps to + * The {@link SwapService} deals with managing the entire workflow from selecting apps to * swap, to invoking this class to prepare the webroot, to enabling various communication protocols. * This class deals specifically with the webroot side of things, ensuring we have a valid index.jar * and the relevant .apk and icon files available. diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java deleted file mode 100644 index d89f8003e..000000000 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java +++ /dev/null @@ -1,461 +0,0 @@ -package org.fdroid.fdroid.localrepo; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.IBinder; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import org.fdroid.fdroid.FDroidApp; -import org.fdroid.fdroid.UpdateService; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.localrepo.peers.Peer; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class SwapManager { - - private static final String TAG = "SwapState"; - private static final String SHARED_PREFERENCES = "swap-state"; - private static final String KEY_APPS_TO_SWAP = "appsToSwap"; - - private static SwapManager instance; - - @NonNull - public static SwapManager load(@NonNull Context context) { - if (instance == null) { - SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); - Set appsToSwap = deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, "")); - instance = new SwapManager(context, appsToSwap); - } - - return instance; - } - - @NonNull - private final Context context; - - @NonNull - private Set appsToSwap; - - @NonNull - private List peers; - - private SwapManager(@NonNull Context context, @NonNull Set appsToSwap) { - this.context = context.getApplicationContext(); - this.appsToSwap = appsToSwap; - this.peers = new ArrayList<>(); - - context.registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Peer peer = (Peer) intent.getSerializableExtra(EXTRA_PEER); - if (!peers.contains(peer)) { - peers.add(peer); - } - } - }, new IntentFilter(ACTION_PEER_FOUND)); - - setupService(); - } - - /** - * 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 context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_APPEND); - } - - // ========================================================== - // Search for peers to swap - // ========================================================== - - public void scanForPeers() { - if (service != null) { - Log.d(TAG, "Scanning for nearby devices to swap with..."); - service.scanForPeers(); - } else { - Log.e(TAG, "Couldn't scan for peers, because service was not running."); - } - } - - public void cancelScanningForPeers() { - if (service != null) { - service.cancelScanningForPeers(); - } else { - Log.e(TAG, "Couldn't cancel scanning for peers, because service was not running."); - } - } - - @NonNull - public List getPeers() { - return peers; - } - - - // ========================================================== - // Manage the current step - // ("Step" refers to the current view being shown in the UI) - // ========================================================== - - public static final int STEP_INTRO = 1; - public static final int STEP_SELECT_APPS = 2; - public static final int STEP_JOIN_WIFI = 3; - public static final int STEP_SHOW_NFC = 4; - public static final int STEP_WIFI_QR = 5; - public static final int STEP_CONNECTING = 6; - public static final int STEP_SUCCESS = 7; - public static final int STEP_CONFIRM_SWAP = 8; - - private @SwapStep int step = STEP_INTRO; - - /** - * Current screen that the swap process is up to. - * Will be one of the SwapState.STEP_* values. - */ - @SwapStep - public int getStep() { - return step; - } - - public SwapManager setStep(@SwapStep int step) { - this.step = step; - return this; - } - - public @NonNull Set getAppsToSwap() { - return appsToSwap; - } - - @Nullable - public UpdateService.UpdateReceiver refreshSwap() { - return this.peer != null ? connectTo(peer) : null; - } - - @NonNull - public UpdateService.UpdateReceiver connectTo(@NonNull Peer peer) { - if (peer != this.peer) { - Log.e(TAG, "Oops, got a different peer to swap with than initially planned."); - } - - peerRepo = ensureRepoExists(peer); - - // Only ask server to swap with us, if we are actually running a local repo service. - // It is possible to have a swap initiated without first starting a swap, in which - // case swapping back is pointless. - /*if (!newRepoConfig.preventFurtherSwaps() && isEnabled()) { - askServerToSwapWithUs(); - }*/ - - return UpdateService.updateRepoNow(peer.getRepoAddress(), context, false); - } -/* - 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", ConnectSwapActivity.this); - 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() { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText( - ConnectSwapActivity.this, - R.string.swap_reciprocate_failed, - Toast.LENGTH_LONG - ).show(); - } - }); - } - }.execute(); - }*/ - private Repo ensureRepoExists(@NonNull Peer peer) { - // 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(context, peer.getRepoAddress()); - 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, peer.getName()); - values.put(RepoProvider.DataColumns.ADDRESS, peer.getRepoAddress()); - values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO; - values.put(RepoProvider.DataColumns.FINGERPRINT, peer.getFingerprint()); - values.put(RepoProvider.DataColumns.IN_USE, true); - values.put(RepoProvider.DataColumns.IS_SWAP, true); - Uri uri = RepoProvider.Helper.insert(context, values); - repo = RepoProvider.Helper.findByUri(context, uri); - } - - return repo; - } - - @Nullable - public Repo getPeerRepo() { - return peerRepo; - } - - public void install(@NonNull final App app) { - - } - - /** - * Ensure that we don't get put into an incorrect state, by forcing people to pass valid - * states to setStep. Ideally this would be done by requiring an enum or something to - * be passed rather than in integer, however that is harder to persist on disk than an int. - * This is the same as, e.g. {@link Context#getSystemService(String)} - */ - @IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR, - STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP}) - @Retention(RetentionPolicy.SOURCE) - public @interface SwapStep {} - - - // ================================================= - // Have selected a specific peer to swap with - // (Rather than showing a generic QR code to scan) - // ================================================= - - @Nullable - private Peer peer; - - @Nullable - private Repo peerRepo; - - public void swapWith(Peer peer) { - this.peer = peer; - } - - public boolean isConnectingWithPeer() { - return peer != null; - } - - @Nullable - public Peer getPeer() { - return peer; - } - - - // ========================================== - // Remember apps user wants to swap - // ========================================== - - private void persistAppsToSwap() { - persistence().edit().putString(KEY_APPS_TO_SWAP, serializePackages(appsToSwap)).commit(); - } - - /** - * Replacement for {@link android.content.SharedPreferences.Editor#putStringSet(String, Set)} - * which is only available in API >= 11. - * Package names are reverse-DNS-style, so they should only have alpha numeric values. Thus, - * this uses a comma as the separator. - * @see SwapManager#deserializePackages(String) - */ - private static String serializePackages(Set packages) { - StringBuilder sb = new StringBuilder(); - for (String pkg : packages) { - if (sb.length() > 0) { - sb.append(','); - } - sb.append(pkg); - } - return sb.toString(); - } - - /** - * @see SwapManager#deserializePackages(String) - */ - private static Set deserializePackages(String packages) { - Set set = new HashSet<>(); - if (!TextUtils.isEmpty(packages)) { - Collections.addAll(set, packages.split(",")); - } - return set; - } - - public void ensureFDroidSelected() { - String fdroid = context.getPackageName(); - if (!hasSelectedPackage(fdroid)) { - selectPackage(fdroid); - } - } - - public boolean hasSelectedPackage(String packageName) { - return appsToSwap.contains(packageName); - } - - public void selectPackage(String packageName) { - appsToSwap.add(packageName); - persistAppsToSwap(); - } - - public void deselectPackage(String packageName) { - if (appsToSwap.contains(packageName)) { - appsToSwap.remove(packageName); - } - persistAppsToSwap(); - } - - - // ========================================== - // Local repo stop/start/restart handling - // ========================================== - - @Nullable - private SwapService service = null; - - private void setupService() { - - ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder binder) { - Log.d(TAG, "Swap service connected, enabling SwapManager to communicate with SwapService."); - service = ((SwapService.Binder)binder).getService(); - } - - @Override - public void onServiceDisconnected(ComponentName className) { - Log.d(TAG, "Swap service disconnected"); - service = null; - } - }; - - // The server should not be doing anything or occupying any (noticable) resources - // until we actually ask it to enable swapping. Therefore, we will start it nice and - // early so we don't have to wait until it is connected later. - Intent service = new Intent(context, SwapService.class); - if (context.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)) { - context.startService(service); - } - - } - - public void enableSwapping() { - if (service != null) { - service.enableSwapping(); - } else { - Log.e(TAG, "Couldn't enable swap, because service was not running."); - } - } - - public void disableSwapping() { - if (service != null) { - service.disableSwapping(); - } else { - Log.e(TAG, "Couldn't disable swap, because service was not running."); - } - setStep(STEP_INTRO); - } - - /** - * Handles checking if the {@link SwapService} is running, and only restarts it if it was running. - */ - public void restartIfEnabled() { - if (service != null) { - service.restartIfEnabled(); - } - } - - public boolean isEnabled() { - return service != null && service.isEnabled(); - } - - // ========================================== - // Interacting with Bluetooth adapter - // ========================================== - - public boolean isBluetoothDiscoverable() { - return service != null && service.getBluetooth().isConnected(); - } - - public void ensureBluetoothDiscoverable() { - if (service != null) { - service.getBluetooth().start(); - } - } - - public void makeBluetoothNonDiscoverable() { - if (service != null) { - service.getBluetooth().stop(); - } - } - - private boolean isWifiConnected() { - return !TextUtils.isEmpty(FDroidApp.ssid); - } - - public boolean isBonjourDiscoverable() { - return isWifiConnected() && service != null && service.isEnabled(); - } - - public void ensureBonjourDiscoverable() { - if (!isBonjourDiscoverable()) { - // TODO: Enable bonjour (currently it is enabled by default when the service starts) - } - } - - public void makeBonjourNotDiscoverable() { - if (service != null) { - // TODO: Disable bonjour (currently it is enabled by default when the service starts) - } - } - - public boolean isScanningForPeers() { - return service != null && service.isScanningForPeers(); - } - - public static final String ACTION_PEER_FOUND = "org.fdroid.fdroid.SwapManager.ACTION_PEER_FOUND"; - public static final String EXTRA_PEER = "EXTRA_PEER"; -} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java index ab0a761c7..c9e0eed39 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java @@ -4,20 +4,32 @@ import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.Uri; import android.os.AsyncTask; import android.os.IBinder; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; import android.util.Log; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.UpdateService; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.localrepo.peers.BluetoothFinder; import org.fdroid.fdroid.localrepo.peers.BonjourFinder; +import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.localrepo.type.BluetoothType; import org.fdroid.fdroid.localrepo.type.BonjourType; import org.fdroid.fdroid.localrepo.type.SwapType; @@ -25,6 +37,12 @@ import org.fdroid.fdroid.localrepo.type.WebServerType; import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -35,105 +53,290 @@ import java.util.TimerTask; */ public class SwapService extends Service { - private static final String TAG = "SwapService"; + private static final String TAG = "SwapManager"; + private static final String SHARED_PREFERENCES = "swap-state"; + private static final String KEY_APPS_TO_SWAP = "appsToSwap"; - public static final String BONJOUR_STATE_CHANGE = "org.fdroid.fdroid.BONJOUR_STATE_CHANGE"; - public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE"; - public static final String EXTRA_STARTING = "STARTING"; - public static final String EXTRA_STARTED = "STARTED"; - public static final String EXTRA_STOPPED = "STOPPED"; + @NonNull + private Set appsToSwap = new HashSet<>(); - private static final int NOTIFICATION = 1; - - private final Binder binder = new Binder(); - private SwapType bonjourType; - private SwapType bluetoothType; - private SwapType webServerType; - - private BonjourFinder bonjourFinder; - private BluetoothFinder bluetoothFinder; - - private final static int TIMEOUT = 900000; // 15 mins + public SwapService() { + super(); + } /** - * Used to automatically turn of swapping after a defined amount of time (15 mins). + * 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. */ - @Nullable - private Timer timer; - - public boolean isScanningForPeers() { - return bonjourFinder.isScanning() || bluetoothFinder.isScanning(); + private SharedPreferences persistence() { + return getSharedPreferences(SHARED_PREFERENCES, MODE_APPEND); } - public SwapType getBluetooth() { - return bluetoothType; - } - - public SwapType getBonjour() { - return bluetoothType; - } - - public class Binder extends android.os.Binder { - public SwapService getService() { - return SwapService.this; - } - } - - public void onCreate() { - super.onCreate(); - - bonjourType = new BonjourType(this); - bluetoothType = BluetoothType.create(this); - webServerType = new WebServerType(this); - bonjourFinder = new BonjourFinder(this); - bluetoothFinder = new BluetoothFinder(this); - - Log.d(TAG, "Creating service, will register appropriate listeners."); - Preferences.get().registerLocalRepoBonjourListeners(bonjourEnabledListener); - Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); - - LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, - new IntentFilter(WifiStateChangeService.BROADCAST)); - } - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); - disableSwapping(); - Preferences.get().unregisterLocalRepoBonjourListeners(bonjourEnabledListener); - Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); - LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); - } - - private Notification createNotification() { - Intent intent = new Intent(this, SwapWorkflowActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - return new NotificationCompat.Builder(this) - .setContentTitle(getText(R.string.local_repo_running)) - .setContentText(getText(R.string.touch_to_configure_local_repo)) - .setSmallIcon(R.drawable.ic_swap) - .setContentIntent(contentIntent) - .build(); - } + // ========================================================== + // Search for peers to swap + // ========================================================== public void scanForPeers() { + Log.d(TAG, "Scanning for nearby devices to swap with..."); bonjourFinder.scan(); bluetoothFinder.scan(); } - public void cancelScanningForPeers() { + public void stopScanningForPeers() { bonjourFinder.cancel(); bluetoothFinder.cancel(); } - private boolean enabled = false; + + // ========================================================== + // Manage the current step + // ("Step" refers to the current view being shown in the UI) + // ========================================================== + + public static final int STEP_INTRO = 1; + public static final int STEP_SELECT_APPS = 2; + public static final int STEP_JOIN_WIFI = 3; + public static final int STEP_SHOW_NFC = 4; + public static final int STEP_WIFI_QR = 5; + public static final int STEP_CONNECTING = 6; + public static final int STEP_SUCCESS = 7; + public static final int STEP_CONFIRM_SWAP = 8; + + /** + * Special view, that we don't really want to actually store against the + * {@link SwapService#step}. Rather, we use it for the purpose of specifying + * we are in the state waiting for the {@link SwapService} to get started and + * bound to the {@link SwapWorkflowActivity}. + */ + public static final int STEP_INITIAL_LOADING = 9; + + private @SwapStep int step = STEP_INTRO; + + /** + * Current screen that the swap process is up to. + * Will be one of the SwapState.STEP_* values. + */ + @SwapStep + public int getStep() { + return step; + } + + public SwapService setStep(@SwapStep int step) { + this.step = step; + return this; + } + + public @NonNull Set getAppsToSwap() { + return appsToSwap; + } + + @Nullable + public UpdateService.UpdateReceiver refreshSwap() { + return this.peer != null ? connectTo(peer) : null; + } + + @NonNull + public UpdateService.UpdateReceiver connectTo(@NonNull Peer peer) { + if (peer != this.peer) { + Log.e(TAG, "Oops, got a different peer to swap with than initially planned."); + } + + peerRepo = ensureRepoExists(peer); + + // Only ask server to swap with us, if we are actually running a local repo service. + // It is possible to have a swap initiated without first starting a swap, in which + // case swapping back is pointless. + /*if (!newRepoConfig.preventFurtherSwaps() && isEnabled()) { + askServerToSwapWithUs(); + }*/ + + return UpdateService.updateRepoNow(peer.getRepoAddress(), this, false); + } +/* + 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", ConnectSwapActivity.this); + 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() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText( + ConnectSwapActivity.this, + R.string.swap_reciprocate_failed, + Toast.LENGTH_LONG + ).show(); + } + }); + } + }.execute(); + }*/ + + private Repo ensureRepoExists(@NonNull Peer peer) { + // 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(this, peer.getRepoAddress()); + 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, peer.getName()); + values.put(RepoProvider.DataColumns.ADDRESS, peer.getRepoAddress()); + values.put(RepoProvider.DataColumns.DESCRIPTION, ""); // TODO; + values.put(RepoProvider.DataColumns.FINGERPRINT, peer.getFingerprint()); + values.put(RepoProvider.DataColumns.IN_USE, true); + values.put(RepoProvider.DataColumns.IS_SWAP, true); + Uri uri = RepoProvider.Helper.insert(this, values); + repo = RepoProvider.Helper.findByUri(this, uri); + } + + return repo; + } + + @Nullable + public Repo getPeerRepo() { + return peerRepo; + } + + public void install(@NonNull final App app) { + + } + + /** + * Ensure that we don't get put into an incorrect state, by forcing people to pass valid + * states to setStep. Ideally this would be done by requiring an enum or something to + * be passed rather than in integer, however that is harder to persist on disk than an int. + * This is the same as, e.g. {@link Context#getSystemService(String)} + */ + @IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR, + STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP, STEP_INITIAL_LOADING}) + @Retention(RetentionPolicy.SOURCE) + public @interface SwapStep {} + + + // ================================================= + // Have selected a specific peer to swap with + // (Rather than showing a generic QR code to scan) + // ================================================= + + @Nullable + private Peer peer; + + @Nullable + private Repo peerRepo; + + public void swapWith(Peer peer) { + this.peer = peer; + } + + public boolean isConnectingWithPeer() { + return peer != null; + } + + @Nullable + public Peer getPeer() { + return peer; + } + + + // ========================================== + // Remember apps user wants to swap + // ========================================== + + private void persistAppsToSwap() { + persistence().edit().putString(KEY_APPS_TO_SWAP, serializePackages(appsToSwap)).commit(); + } + + /** + * Replacement for {@link android.content.SharedPreferences.Editor#putStringSet(String, Set)} + * which is only available in API >= 11. + * Package names are reverse-DNS-style, so they should only have alpha numeric values. Thus, + * this uses a comma as the separator. + * @see SwapService#deserializePackages(String) + */ + private static String serializePackages(Set packages) { + StringBuilder sb = new StringBuilder(); + for (String pkg : packages) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(pkg); + } + return sb.toString(); + } + + /** + * @see SwapService#deserializePackages(String) + */ + private static Set deserializePackages(String packages) { + Set set = new HashSet<>(); + if (!TextUtils.isEmpty(packages)) { + Collections.addAll(set, packages.split(",")); + } + return set; + } + + public void ensureFDroidSelected() { + String fdroid = getPackageName(); + if (!hasSelectedPackage(fdroid)) { + selectPackage(fdroid); + } + } + + public boolean hasSelectedPackage(String packageName) { + return appsToSwap.contains(packageName); + } + + public void selectPackage(String packageName) { + appsToSwap.add(packageName); + persistAppsToSwap(); + } + + public void deselectPackage(String packageName) { + if (appsToSwap.contains(packageName)) { + appsToSwap.remove(packageName); + } + persistAppsToSwap(); + } + + + // ========================================== + // Local repo stop/start/restart handling + // ========================================== /** * Ensures that the webserver is running, as are the other services which make swap work. @@ -165,17 +368,6 @@ public class SwapService extends Service { initTimer(); } - /** - * The guts of this class - responsible for enabling the relevant services for swapping. - * Doesn't know anything about enabled/disabled state, you should check that before invoking - * this method so it doesn't start something that is already started. - * Runs asynchronously on several background threads. - */ - private void enableSwappingAsynchronous() { - webServerType.startInBackground(); - bonjourType.startInBackground(); - } - public void disableSwapping() { if (enabled) { new AsyncTask() { @@ -206,18 +398,8 @@ public class SwapService extends Service { } /** - * @see SwapService#enableSwappingAsynchronous() + * Handles checking if the {@link SwapService} is running, and only restarts it if it was running. */ - private void disableSwappingSynchronous() { - Log.d(TAG, "Disabling SwapService (bonjour, webserver, etc)"); - bonjourType.stop(); - webServerType.stop(); - } - - public boolean isEnabled() { - return enabled; - } - public void restartIfEnabled() { if (enabled) { new AsyncTask() { @@ -232,6 +414,169 @@ public class SwapService extends Service { } } + public boolean isEnabled() { + return enabled; + } + + // ========================================== + // Interacting with Bluetooth adapter + // ========================================== + + public boolean isBluetoothDiscoverable() { + return bluetoothType.isConnected(); + } + + public void ensureBluetoothDiscoverable() { + bluetoothType.start(); + } + + public void makeBluetoothNonDiscoverable() { + bluetoothType.stop(); + } + + private boolean isWifiConnected() { + return !TextUtils.isEmpty(FDroidApp.ssid); + } + + public boolean isBonjourDiscoverable() { + return isWifiConnected() && isEnabled(); + } + + public void ensureBonjourDiscoverable() { + if (!isBonjourDiscoverable()) { + // TODO: Enable bonjour (currently it is enabled by default when the service starts) + } + } + + public void makeBonjourNotDiscoverable() { + // TODO: Disable bonjour (currently it is enabled by default when the service starts) + } + + public boolean isScanningForPeers() { + return bonjourFinder.isScanning() || bluetoothFinder.isScanning(); + } + + 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. + // =============================================================== + + public static final String BONJOUR_STATE_CHANGE = "org.fdroid.fdroid.BONJOUR_STATE_CHANGE"; + public static final String BLUETOOTH_STATE_CHANGE = "org.fdroid.fdroid.BLUETOOTH_STATE_CHANGE"; + public static final String EXTRA_STARTING = "STARTING"; + public static final String EXTRA_STARTED = "STARTED"; + public static final String EXTRA_STOPPED = "STOPPED"; + + private static final int NOTIFICATION = 1; + + private final Binder binder = new Binder(); + private SwapType bonjourType; + private SwapType bluetoothType; + private SwapType webServerType; + + private BonjourFinder bonjourFinder; + private BluetoothFinder bluetoothFinder; + + private final static int TIMEOUT = 900000; // 15 mins + + /** + * Used to automatically turn of swapping after a defined amount of time (15 mins). + */ + @Nullable + private Timer timer; + + public SwapType getBluetooth() { + return bluetoothType; + } + + public SwapType getBonjour() { + return bluetoothType; + } + + public class Binder extends android.os.Binder { + public SwapService getService() { + return SwapService.this; + } + } + + public void onCreate() { + super.onCreate(); + + Log.d(TAG, "Creating swap service."); + + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); + + appsToSwap.addAll(deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, ""))); + bonjourType = new BonjourType(this); + bluetoothType = BluetoothType.create(this); + webServerType = new WebServerType(this); + bonjourFinder = new BonjourFinder(this); + bluetoothFinder = new BluetoothFinder(this); + + Preferences.get().registerLocalRepoBonjourListeners(bonjourEnabledListener); + Preferences.get().registerLocalRepoHttpsListeners(httpsEnabledListener); + + LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, new IntentFilter(WifiStateChangeService.BROADCAST)); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); + disableSwapping(); + Preferences.get().unregisterLocalRepoBonjourListeners(bonjourEnabledListener); + Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); + LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); + } + + private Notification createNotification() { + Intent intent = new Intent(this, SwapWorkflowActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + return new NotificationCompat.Builder(this) + .setContentTitle(getText(R.string.local_repo_running)) + .setContentText(getText(R.string.touch_to_configure_local_repo)) + .setSmallIcon(R.drawable.ic_swap) + .setContentIntent(contentIntent) + .build(); + } + + private boolean enabled = false; + + /** + * The guts of this class - responsible for enabling the relevant services for swapping. + * Doesn't know anything about enabled/disabled state, you should check that before invoking + * this method so it doesn't start something that is already started. + * Runs asynchronously on several background threads. + */ + private void enableSwappingAsynchronous() { + webServerType.startInBackground(); + bonjourType.startInBackground(); + } + + /** + * @see SwapService#enableSwappingAsynchronous() + */ + private void disableSwappingSynchronous() { + Log.d(TAG, "Disabling SwapService (bonjour, webserver, etc)"); + bonjourType.stop(); + webServerType.stop(); + } + private void initTimer() { if (timer != null) { Log.d(TAG, "Cancelling existing timer"); diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java index 452bda7c2..aa62f88b1 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/peers/PeerFinder.java @@ -2,14 +2,13 @@ package org.fdroid.fdroid.localrepo.peers; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.util.Log; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; /** * Searches for other devices in the vicinity, using specific technologies. - * Once found, sends an {@link SwapManager#ACTION_PEER_FOUND} intent with the {@link SwapManager#EXTRA_PEER} + * Once found, sends an {@link SwapService#ACTION_PEER_FOUND} intent with the {@link SwapService#EXTRA_PEER} * extra attribute set to the subclass of {@link Peer} that was found. */ public abstract class PeerFinder { @@ -32,8 +31,8 @@ public abstract class PeerFinder { protected void foundPeer(T peer) { Log.i(TAG, "Found peer " + peer.getName()); - Intent intent = new Intent(SwapManager.ACTION_PEER_FOUND); - intent.putExtra(SwapManager.EXTRA_PEER, peer); + Intent intent = new Intent(SwapService.ACTION_PEER_FOUND); + intent.putExtra(SwapService.EXTRA_PEER, peer); context.sendBroadcast(intent); } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java index 18a4b4e71..f6b6ac099 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java @@ -3,7 +3,6 @@ package org.fdroid.fdroid.localrepo.type; import android.content.Context; import android.util.Log; -import org.fdroid.fdroid.FDroid; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java index 3b5171a26..df688998a 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -1,8 +1,10 @@ package org.fdroid.fdroid.net; import android.app.Service; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -17,7 +19,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.localrepo.LocalRepoKeyStore; import org.fdroid.fdroid.localrepo.LocalRepoManager; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import java.net.Inet6Address; import java.net.InetAddress; @@ -159,7 +161,17 @@ public class WifiStateChangeService extends Service { Intent intent = new Intent(BROADCAST); LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent); WifiStateChangeService.this.stopSelf(); - SwapManager.load(WifiStateChangeService.this).restartIfEnabled(); + + Intent swapService = new Intent(WifiStateChangeService.this, SwapService.class); + bindService(swapService, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + ((SwapService.Binder)service).getService().restartIfEnabled(); + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }, BIND_AUTO_CREATE); } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java index 15382b20b..70367326f 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConfirmReceive.java @@ -5,18 +5,13 @@ import android.content.Context; import android.os.Build; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; -import android.support.v4.view.MenuItemCompat; import android.util.AttributeSet; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.RelativeLayout; -import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; public class ConfirmReceive extends RelativeLayout implements SwapWorkflowActivity.InnerView { @@ -41,7 +36,7 @@ public class ConfirmReceive extends RelativeLayout implements SwapWorkflowActivi return (SwapWorkflowActivity)getContext(); } - private SwapManager getManager() { + private SwapService getManager() { return getActivity().getState(); } @@ -59,12 +54,12 @@ public class ConfirmReceive extends RelativeLayout implements SwapWorkflowActivi @Override public int getStep() { - return SwapManager.STEP_CONFIRM_SWAP; + return SwapService.STEP_CONFIRM_SWAP; } @Override public int getPreviousStep() { - return SwapManager.STEP_INTRO; + return SwapService.STEP_INTRO; } @ColorRes 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 aa682b99a..76d662806 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java @@ -1,12 +1,15 @@ package org.fdroid.fdroid.views.swap; import android.app.Activity; +import android.content.ComponentName; import android.content.ContentValues; import android.content.Intent; +import android.content.ServiceConnection; import android.net.Uri; import android.net.http.AndroidHttpClient; import android.os.AsyncTask; import android.os.Bundle; +import android.os.IBinder; import android.support.annotation.Nullable; import android.support.v7.app.ActionBarActivity; import android.util.Log; @@ -27,7 +30,7 @@ import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -152,16 +155,39 @@ public class ConnectSwapActivity extends ActionBarActivity implements ProgressLi repo = RepoProvider.Helper.findByUri(this, uri); } - // Only ask server to swap with us, if we are actually running a local repo service. - // It is possible to have a swap initiated without first starting a swap, in which - // case swapping back is pointless. - if (!newRepoConfig.preventFurtherSwaps() && SwapManager.load(this).isEnabled()) { - askServerToSwapWithUs(); - } + attemptSwapBack(); return repo; } + /** + * Only ask server to swap with us, if we are actually running a local repo service. + * It is possible to have a swap initiated without first starting a swap, in which + * case swapping back is pointless. + */ + private void attemptSwapBack() { + + if (newRepoConfig.preventFurtherSwaps()) { + return; + } + + ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + SwapService service = ((SwapService.Binder) binder).getService(); + if (service.isEnabled()) { + askServerToSwapWithUs(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + + Intent intent = new Intent(this, SwapService.class); + bindService(intent, connection, BIND_AUTO_CREATE); + } + private void askServerToSwapWithUs() { if (!newRepoConfig.isValidRepo()) { return; diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/InitialLoadingView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/InitialLoadingView.java new file mode 100644 index 000000000..d3be23001 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/InitialLoadingView.java @@ -0,0 +1,68 @@ +package org.fdroid.fdroid.views.swap; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.v4.view.MenuItemCompat; +import android.util.AttributeSet; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.RelativeLayout; + +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.localrepo.SwapService; + +public class InitialLoadingView extends RelativeLayout implements SwapWorkflowActivity.InnerView { + + public InitialLoadingView(Context context) { + super(context); + } + + public InitialLoadingView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InitialLoadingView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public InitialLoadingView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private SwapWorkflowActivity getActivity() { + return (SwapWorkflowActivity)getContext(); + } + + @Override + public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) { + return true; + } + + @Override + public int getStep() { + return SwapService.STEP_INITIAL_LOADING; + } + + @Override + public int getPreviousStep() { + return SwapService.STEP_JOIN_WIFI; + } + + @ColorRes + public int getToolbarColour() { + return getResources().getColor(R.color.swap_blue); + } + + @Override + public String getToolbarTitle() { + return getResources().getString(R.string.swap); + } +} diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java index b606b3ddf..7f352fae2 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java @@ -23,7 +23,7 @@ import android.widget.TextView; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.net.WifiStateChangeService; public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity.InnerView { @@ -122,12 +122,12 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity @Override public int getStep() { - return SwapManager.STEP_JOIN_WIFI; + return SwapService.STEP_JOIN_WIFI; } @Override public int getPreviousStep() { - return SwapManager.STEP_INTRO; + return SwapService.STEP_INTRO; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java index ab8da2d32..d5bd3d251 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java @@ -16,7 +16,7 @@ import android.widget.RelativeLayout; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; public class NfcView extends RelativeLayout implements SwapWorkflowActivity.InnerView { @@ -70,12 +70,12 @@ public class NfcView extends RelativeLayout implements SwapWorkflowActivity.Inne @Override public int getStep() { - return SwapManager.STEP_SHOW_NFC; + return SwapService.STEP_SHOW_NFC; } @Override public int getPreviousStep() { - return SwapManager.STEP_JOIN_WIFI; + return SwapService.STEP_JOIN_WIFI; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java index 6651cbf19..b8288830f 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java @@ -36,7 +36,7 @@ import android.widget.TextView; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.InstalledAppProvider; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; public class SelectAppsView extends ListView implements SwapWorkflowActivity.InnerView, @@ -64,7 +64,7 @@ public class SelectAppsView extends ListView implements return (SwapWorkflowActivity)getContext(); } - private SwapManager getState() { + private SwapService getState() { return getActivity().getState(); } @@ -119,12 +119,12 @@ public class SelectAppsView extends ListView implements @Override public int getStep() { - return SwapManager.STEP_SELECT_APPS; + return SwapService.STEP_SELECT_APPS; } @Override public int getPreviousStep() { - return getState().isConnectingWithPeer() ? SwapManager.STEP_JOIN_WIFI : SwapManager.STEP_INTRO; + return getState().isConnectingWithPeer() ? SwapService.STEP_JOIN_WIFI : SwapService.STEP_INTRO; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java index abd1a32db..5a042378d 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java @@ -6,8 +6,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.os.Build; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; @@ -23,10 +21,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.CompoundButton; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.ScrollView; @@ -35,7 +31,6 @@ import android.widget.TextView; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.localrepo.SwapManager; import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; import org.fdroid.fdroid.net.WifiStateChangeService; @@ -97,7 +92,7 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In return (SwapWorkflowActivity)getContext(); } - private SwapManager getManager() { + private SwapService getManager() { return getActivity().getState(); } @@ -175,7 +170,7 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In getContext().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Peer peer = intent.getParcelableExtra(SwapManager.EXTRA_PEER); + Peer peer = intent.getParcelableExtra(SwapService.EXTRA_PEER); if (adapter.getPosition(peer) >= 0) { Log.d(TAG, "Found peer: " + peer + ", ignoring though, because it is already in our list."); } else { @@ -184,7 +179,7 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In uiUpdatePeersInfo(); } } - }, new IntentFilter(SwapManager.ACTION_PEER_FOUND)); + }, new IntentFilter(SwapService.ACTION_PEER_FOUND)); } @@ -241,12 +236,12 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In if (intent.hasExtra(SwapService.EXTRA_STARTING)) { Log.d(TAG, "Bluetooth service is starting..."); bluetoothSwitch.setEnabled(false); - bluetoothSwitch.setChecked(true); + // bluetoothSwitch.setChecked(true); } else { bluetoothSwitch.setEnabled(true); if (intent.hasExtra(SwapService.EXTRA_STARTED)) { Log.d(TAG, "Bluetooth service has started."); - bluetoothSwitch.setChecked(true); + // bluetoothSwitch.setChecked(true); } else { Log.d(TAG, "Bluetooth service has stopped."); bluetoothSwitch.setChecked(false); @@ -336,7 +331,7 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In @Override public int getStep() { - return SwapManager.STEP_INTRO; + return SwapService.STEP_INTRO; } @Override @@ -345,7 +340,7 @@ public class StartSwapView extends ScrollView implements SwapWorkflowActivity.In // if getStep is STEP_INTRO, don't even bother asking for getPreviousStep. But that is a // bit messy. It would be nicer if this was handled using the same mechanism as everything // else. - return SwapManager.STEP_INTRO; + return SwapService.STEP_INTRO; } @Override diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java index fc7089c8c..cadef72f6 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapAppsView.java @@ -20,7 +20,6 @@ import android.support.v7.widget.SearchView; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; -import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -39,7 +38,7 @@ import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import java.util.Timer; import java.util.TimerTask; @@ -70,7 +69,7 @@ public class SwapAppsView extends ListView implements return (SwapWorkflowActivity)getContext(); } - private SwapManager getState() { + private SwapService getState() { return getActivity().getState(); } @@ -175,12 +174,12 @@ public class SwapAppsView extends ListView implements @Override public int getStep() { - return SwapManager.STEP_SUCCESS; + return SwapService.STEP_SUCCESS; } @Override public int getPreviousStep() { - return SwapManager.STEP_INTRO; + return SwapService.STEP_INTRO; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java index a4d6015b3..ba0d34e33 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapConnecting.java @@ -5,28 +5,19 @@ import android.content.Context; import android.os.Build; import android.support.annotation.ColorRes; import android.support.annotation.NonNull; -import android.support.v4.view.MenuItemCompat; import android.util.AttributeSet; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; -import android.view.MenuItem; -import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.LinearLayout; -import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; -import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; -import java.util.List; - public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity.InnerView { private final static String TAG = "SwapConnecting"; @@ -52,7 +43,7 @@ public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity return (SwapWorkflowActivity)getContext(); } - private SwapManager getManager() { + private SwapService getManager() { return getActivity().getState(); } @@ -116,12 +107,12 @@ public class SwapConnecting extends LinearLayout implements SwapWorkflowActivity @Override public int getStep() { - return SwapManager.STEP_CONNECTING; + return SwapService.STEP_CONNECTING; } @Override public int getPreviousStep() { - return SwapManager.STEP_SELECT_APPS; + return SwapService.STEP_SELECT_APPS; } @ColorRes diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 7d20102a8..0a73d70de 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -2,14 +2,18 @@ package org.fdroid.fdroid.views.swap; import android.app.Activity; import android.app.ProgressDialog; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.IBinder; import android.support.annotation.ColorRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -31,7 +35,7 @@ import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.localrepo.LocalRepoManager; -import org.fdroid.fdroid.localrepo.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.localrepo.peers.Peer; import java.util.Arrays; @@ -45,7 +49,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { /** * A UI component (subclass of {@link View}) which forms part of the swap workflow. * There is a one to one mapping between an {@link org.fdroid.fdroid.views.swap.SwapWorkflowActivity.InnerView} - * and a {@link org.fdroid.fdroid.localrepo.SwapManager.SwapStep}, and these views know what + * and a {@link SwapService.SwapStep}, and these views know what * the previous view before them should be. */ public interface InnerView { @@ -53,9 +57,9 @@ public class SwapWorkflowActivity extends AppCompatActivity { boolean buildMenu(Menu menu, @NonNull MenuInflater inflater); /** @return The step that this view represents. */ - @SwapManager.SwapStep int getStep(); + @SwapService.SwapStep int getStep(); - @SwapManager.SwapStep int getPreviousStep(); + @SwapService.SwapStep int getPreviousStep(); @ColorRes int getToolbarColour(); @@ -66,18 +70,57 @@ public class SwapWorkflowActivity extends AppCompatActivity { private static final int CONNECT_TO_SWAP = 1; private Toolbar toolbar; - private SwapManager state; private InnerView currentView; private boolean hasPreparedLocalRepo = false; private PrepareSwapRepo updateSwappableAppsTask = null; + @Nullable + private SwapService service = null; + + @NonNull + public SwapService getService() { + if (service == null) { + // *Slightly* more informative than a null-pointer error that would otherwise happen. + throw new IllegalStateException("Trying to access swap service before it was initialized."); + } + return service; + } + + private void setupService() { + + ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder binder) { + Log.d(TAG, "Swap service connected, enabling SwapManager to communicate with SwapService."); + service = ((SwapService.Binder)binder).getService(); + showRelevantView(); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Log.d(TAG, "Swap service disconnected"); + service = null; + // TODO: What to do about the UI in this instance? + } + }; + + // The server should not be doing anything or occupying any (noticeable) resources + // until we actually ask it to enable swapping. Therefore, we will start it nice and + // early so we don't have to wait until it is connected later. + Intent service = new Intent(this, SwapService.class); + if (bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)) { + startService(service); + } + + } + @Override public void onBackPressed() { - if (currentView.getStep() == SwapManager.STEP_INTRO) { + if (currentView.getStep() == SwapService.STEP_INTRO) { finish(); } else { int nextStep = currentView.getPreviousStep(); - state.setStep(nextStep); + getService().setStep(nextStep); showRelevantView(); } } @@ -85,7 +128,9 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - state = SwapManager.load(this); + + setupService(); + setContentView(R.layout.swap_activity); toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -93,14 +138,13 @@ public class SwapWorkflowActivity extends AppCompatActivity { setSupportActionBar(toolbar); container = (ViewGroup) findViewById(R.id.fragment_container); - showRelevantView(); } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); boolean parent = super.onPrepareOptionsMenu(menu); - boolean inner = currentView.buildMenu(menu, getMenuInflater()); + boolean inner = currentView != null && currentView.buildMenu(menu, getMenuInflater()); return parent || inner; } @@ -111,32 +155,38 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void showRelevantView() { - if (currentView != null && currentView.getStep() == state.getStep()) { - // Already showing the currect step, so don't bother changing anything. + + if (service == null) { + showInitialLoading(); return; } - switch(state.getStep()) { - case SwapManager.STEP_INTRO: + if (container.getVisibility() == View.GONE || currentView != null && currentView.getStep() == service.getStep()) { + // Already showing the correct step, so don't bother changing anything. + return; + } + + switch(service.getStep()) { + case SwapService.STEP_INTRO: showIntro(); break; - case SwapManager.STEP_SELECT_APPS: + case SwapService.STEP_SELECT_APPS: showSelectApps(); break; - case SwapManager.STEP_SHOW_NFC: + case SwapService.STEP_SHOW_NFC: showNfc(); break; - case SwapManager.STEP_JOIN_WIFI: + case SwapService.STEP_JOIN_WIFI: showJoinWifi(); break; - case SwapManager.STEP_WIFI_QR: + case SwapService.STEP_WIFI_QR: showWifiQr(); break; } } - public SwapManager getState() { - return state; + public SwapService getState() { + return service; } private void showNfc() { @@ -149,7 +199,16 @@ public class SwapWorkflowActivity extends AppCompatActivity { container.removeAllViews(); View view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(viewRes, container, false); currentView = (InnerView)view; - state.setStep(currentView.getStep()); + + // Don't actually set the step to STEP_INITIAL_LOADING, as we are going to use this view + // purely as a placeholder for _whatever view is meant to be shown_. + if (currentView.getStep() != SwapService.STEP_INITIAL_LOADING) { + if (service == null) { + throw new IllegalStateException("We are not in the STEP_INITIAL_LOADING state, but the service is not ready."); + } + service.setStep(currentView.getStep()); + } + toolbar.setBackgroundColor(currentView.getToolbarColour()); toolbar.setTitle(currentView.getToolbarTitle()); toolbar.setNavigationIcon(R.drawable.ic_close_white); @@ -164,15 +223,19 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void onToolbarCancel() { - SwapManager.load(this).disableSwapping(); + getService().disableSwapping(); finish(); } + private void showInitialLoading() { + inflateInnerView(R.layout.swap_initial_loading); + } + private void showIntro() { - if (!state.isEnabled()) { + if (!getService().isEnabled()) { prepareInitialRepo(); } - SwapManager.load(this).scanForPeers(); + getService().scanForPeers(); inflateInnerView(R.layout.swap_blank); } @@ -190,7 +253,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { // Otherwise, probably will need to scan the file system. public void onAppsSelected() { if (updateSwappableAppsTask == null && !hasPreparedLocalRepo) { - updateSwappableAppsTask = new PrepareFullSwapRepo(state.getAppsToSwap()); + updateSwappableAppsTask = new PrepareFullSwapRepo(getService().getAppsToSwap()); updateSwappableAppsTask.execute(); } else if (!attemptToShowNfc()) { showWifiQr(); @@ -218,11 +281,11 @@ public class SwapWorkflowActivity extends AppCompatActivity { private void onLocalRepoPrepared() { updateSwappableAppsTask = null; hasPreparedLocalRepo = true; - if (state.isConnectingWithPeer()) { + if (getService().isConnectingWithPeer()) { startSwappingWithPeer(); } else if (!attemptToShowNfc()) { showWifiQr(); - }; + } } private void startSwappingWithPeer() { @@ -263,7 +326,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } public void swapWith(Peer peer) { - state.swapWith(peer); + getService().swapWith(peer); showSelectApps(); } @@ -291,7 +354,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override protected void onPreExecute() { - state.enableSwapping(); + getService().enableSwapping(); super.onPreExecute(); } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java index 8737524f2..c2e8354d0 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java @@ -31,7 +31,7 @@ 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.SwapManager; +import org.fdroid.fdroid.localrepo.SwapService; import org.fdroid.fdroid.net.WifiStateChangeService; import java.net.URI; @@ -114,13 +114,13 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner @Override public int getStep() { - return SwapManager.STEP_WIFI_QR; + return SwapService.STEP_WIFI_QR; } @Override public int getPreviousStep() { // TODO: Find a way to make this optionally go back to the NFC screen if appropriate. - return SwapManager.STEP_JOIN_WIFI; + return SwapService.STEP_JOIN_WIFI; } @ColorRes