Refactored SwapService + SwapManager into single SwapService

This commit is contained in:
Peter Serwylo 2015-07-09 11:40:28 +10:00
parent a3af6b8b9f
commit 0b1e2732c2
20 changed files with 729 additions and 671 deletions

View File

@ -6,16 +6,16 @@
android:orientation="vertical">
<android.support.v7.widget.Toolbar
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/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" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
android:id="@+id/fragment_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<org.fdroid.fdroid.views.swap.InitialLoadingView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/swap_blue"
android:paddingTop="38.8dp"> <!-- 69px * 96dpi / 160dpi -->
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textSize="18sp"
android:layout_below="@+id/progress"
android:textColor="@android:color/white"
android:layout_centerHorizontal="true" />
</org.fdroid.fdroid.views.swap.InitialLoadingView>

View File

@ -347,4 +347,5 @@
<string name="wifi_warning_public">May work</string>
<string name="wifi_warning_private">Promising</string>
<string name="wifi_warning_personal">Best bet</string>
<string name="loading">Loading...</string>
</resources>

View File

@ -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.

View File

@ -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<String> appsToSwap = deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, ""));
instance = new SwapManager(context, appsToSwap);
}
return instance;
}
@NonNull
private final Context context;
@NonNull
private Set<String> appsToSwap;
@NonNull
private List<Peer> peers;
private SwapManager(@NonNull Context context, @NonNull Set<String> 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<Peer> 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<String> 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<Void, Void, Void>() {
@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<NameValuePair> 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<String> 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<String> deserializePackages(String packages) {
Set<String> 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";
}

View File

@ -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<String> 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<String> 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<Void, Void, Void>() {
@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<NameValuePair> 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<String> 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<String> deserializePackages(String packages) {
Set<String> 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<Void, Void, Void>() {
@ -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<Void, Void, Void>() {
@ -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");

View File

@ -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<T extends Peer> {
@ -32,8 +31,8 @@ public abstract class PeerFinder<T extends Peer> {
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);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}
}

View File

@ -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